From b735d3b57410e26a01ef047569ffb5f0ef267b67 Mon Sep 17 00:00:00 2001 From: Ed Morley <501702+edmorley@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:49:29 +0100 Subject: [PATCH] Misc integration test improvements As part of adding Poetry + `.python-version` file support, a fair amount of buildpack refactoring will be required. This backports some of the additional test scenarios we now have in the new Python CNB, along with some test general improvements. It's been split out of later PRs for easier review. --- .github/workflows/ci.yml | 6 +- Gemfile.lock | 24 +-- bin/steps/pipenv | 1 + spec/fixtures/ci_pipenv/Pipfile | 10 + spec/fixtures/ci_pipenv/Pipfile.lock | 62 ++++++ spec/fixtures/ci_pipenv/app.json | 9 + spec/fixtures/ci_pipenv/bin/compile | 12 ++ spec/fixtures/ci_pipenv/bin/detect | 7 + spec/fixtures/ci_pipenv/bin/post_compile | 5 + spec/fixtures/ci_pipenv/bin/print-env-vars.sh | 5 + spec/fixtures/ci_requirements/app.json | 2 +- spec/fixtures/ci_requirements/bin/compile | 12 ++ spec/fixtures/ci_requirements/bin/detect | 7 + .../fixtures/ci_requirements/bin/post_compile | 5 + .../ci_requirements/bin/print-env-vars.sh | 5 + .../ci_requirements/requirements-test.txt | 2 +- .../fixtures/ci_requirements/requirements.txt | 3 +- spec/fixtures/hooks/bin/post_compile | 5 +- spec/fixtures/hooks/bin/pre_compile | 5 +- spec/fixtures/hooks/bin/print-env-vars.sh | 10 + spec/fixtures/pipenv_editable/bin/compile | 2 +- .../fixtures/pipenv_editable/bin/post_compile | 2 +- .../{test-entrypoints => test-entrypoints.sh} | 0 .../pipenv_python_version_unspecified/Pipfile | 2 +- .../Pipfile.lock | 10 +- .../bin/compile | 19 ++ .../bin/detect | 7 + .../setup.py | 9 + .../fixtures/python_3.10_outdated/runtime.txt | 2 +- spec/fixtures/python_3.8_outdated/runtime.txt | 2 +- spec/fixtures/python_3.9_outdated/runtime.txt | 2 +- spec/fixtures/requirements_basic/bin/compile | 18 ++ spec/fixtures/requirements_basic/bin/detect | 7 + .../requirements_basic/requirements.txt | 2 + .../requirements_editable/bin/compile | 2 +- .../requirements_editable/bin/post_compile | 2 +- .../{test-entrypoints => test-entrypoints.sh} | 0 .../requirements_git/requirements.txt | 2 - .../requirements_invalid/requirements.txt | 1 + spec/hatchet/ci_spec.rb | 197 ++++++++++++++---- spec/hatchet/django_spec.rb | 11 +- spec/hatchet/hooks_spec.rb | 83 ++++---- spec/hatchet/package_manager_spec.rb | 1 + spec/hatchet/pip_spec.rb | 134 +++++++++--- spec/hatchet/pipenv_spec.rb | 75 ++++++- spec/hatchet/python_update_warning_spec.rb | 20 +- spec/hatchet/python_version_spec.rb | 3 + spec/spec_helper.rb | 2 + 48 files changed, 659 insertions(+), 155 deletions(-) create mode 100644 spec/fixtures/ci_pipenv/Pipfile create mode 100644 spec/fixtures/ci_pipenv/Pipfile.lock create mode 100644 spec/fixtures/ci_pipenv/app.json create mode 100755 spec/fixtures/ci_pipenv/bin/compile create mode 100755 spec/fixtures/ci_pipenv/bin/detect create mode 100644 spec/fixtures/ci_pipenv/bin/post_compile create mode 100755 spec/fixtures/ci_pipenv/bin/print-env-vars.sh create mode 100755 spec/fixtures/ci_requirements/bin/compile create mode 100755 spec/fixtures/ci_requirements/bin/detect create mode 100644 spec/fixtures/ci_requirements/bin/post_compile create mode 100755 spec/fixtures/ci_requirements/bin/print-env-vars.sh create mode 100755 spec/fixtures/hooks/bin/print-env-vars.sh rename spec/fixtures/pipenv_editable/bin/{test-entrypoints => test-entrypoints.sh} (100%) create mode 100755 spec/fixtures/pipenv_python_version_unspecified/bin/compile create mode 100755 spec/fixtures/pipenv_python_version_unspecified/bin/detect create mode 100644 spec/fixtures/pipenv_python_version_unspecified/setup.py create mode 100755 spec/fixtures/requirements_basic/bin/compile create mode 100755 spec/fixtures/requirements_basic/bin/detect create mode 100644 spec/fixtures/requirements_basic/requirements.txt rename spec/fixtures/requirements_editable/bin/{test-entrypoints => test-entrypoints.sh} (100%) delete mode 100644 spec/fixtures/requirements_git/requirements.txt create mode 100644 spec/fixtures/requirements_invalid/requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed5588b7d..d7f97513f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,14 +33,14 @@ jobs: matrix: stack: ["heroku-20", "heroku-22", "heroku-24"] env: - HATCHET_APP_LIMIT: 200 + HATCHET_APP_LIMIT: 300 HATCHET_DEFAULT_STACK: ${{ matrix.stack }} HATCHET_EXPENSIVE_MODE: 1 HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} HEROKU_API_USER: ${{ secrets.HEROKU_API_USER }} HEROKU_DISABLE_AUTOUPDATE: 1 - PARALLEL_SPLIT_TEST_PROCESSES: 60 - RSPEC_RETRY_RETRY_COUNT: 3 + PARALLEL_SPLIT_TEST_PROCESSES: 70 + RSPEC_RETRY_RETRY_COUNT: 2 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/Gemfile.lock b/Gemfile.lock index 6dc35eb1b..7b8aeedce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,7 @@ GEM base64 (0.2.0) diff-lcs (1.5.1) erubis (2.7.0) - excon (0.110.0) + excon (0.111.0) heroics (0.1.3) base64 erubis (~> 2.0) @@ -27,7 +27,7 @@ GEM parallel_split_test (0.10.0) parallel (>= 0.5.13) rspec-core (>= 3.9.0) - parser (3.3.4.2) + parser (3.3.5.0) ast (~> 2.4.1) racc platform-api (3.7.0) @@ -39,32 +39,32 @@ GEM rate_throttle_client (0.1.2) regexp_parser (2.9.2) rrrretry (1.0.0) - rspec-core (3.13.0) + rspec-core (3.13.1) rspec-support (~> 3.13.0) - rspec-expectations (3.13.2) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.13.1) - rubocop (1.66.0) + rubocop (1.66.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.4, < 3.0) - rubocop-ast (>= 1.32.1, < 2.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.1) + rubocop-ast (1.32.3) parser (>= 3.3.1.0) - rubocop-rspec (3.0.4) + rubocop-rspec (3.0.5) rubocop (~> 1.61) ruby-progressbar (1.13.0) - thor (1.3.1) + thor (1.3.2) threaded (0.0.4) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) webrick (1.8.1) PLATFORMS @@ -80,7 +80,7 @@ DEPENDENCIES rubocop-rspec RUBY VERSION - ruby 3.3.2p78 + ruby 3.3.5p100 BUNDLED WITH - 2.5.11 + 2.5.18 diff --git a/bin/steps/pipenv b/bin/steps/pipenv index d7a190b4b..1d97941c7 100755 --- a/bin/steps/pipenv +++ b/bin/steps/pipenv @@ -15,6 +15,7 @@ if [[ -f Pipfile ]]; then meta_set "package_manager" "pipenv" # Skip installing dependencies using pip later. + # TODO: Stop leaking this env var into subshells such as post_compile hooks. export SKIP_PIP_INSTALL=1 # Set Pip env vars diff --git a/spec/fixtures/ci_pipenv/Pipfile b/spec/fixtures/ci_pipenv/Pipfile new file mode 100644 index 000000000..6d6473eb4 --- /dev/null +++ b/spec/fixtures/ci_pipenv/Pipfile @@ -0,0 +1,10 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +typing-extensions = "*" + +[dev-packages] +pytest = "*" diff --git a/spec/fixtures/ci_pipenv/Pipfile.lock b/spec/fixtures/ci_pipenv/Pipfile.lock new file mode 100644 index 000000000..aa11d8ecf --- /dev/null +++ b/spec/fixtures/ci_pipenv/Pipfile.lock @@ -0,0 +1,62 @@ +{ + "_meta": { + "hash": { + "sha256": "9ac30f761973e7bb9a0425635eb284370fede0e49a74b475c418f98bf13f3075" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.3.3" + } + } +} diff --git a/spec/fixtures/ci_pipenv/app.json b/spec/fixtures/ci_pipenv/app.json new file mode 100644 index 000000000..59617c169 --- /dev/null +++ b/spec/fixtures/ci_pipenv/app.json @@ -0,0 +1,9 @@ +{ + "environments": { + "test": { + "scripts": { + "test": "./bin/print-env-vars.sh && pytest --version" + } + } + } +} diff --git a/spec/fixtures/ci_pipenv/bin/compile b/spec/fixtures/ci_pipenv/bin/compile new file mode 100755 index 000000000..fcb7055e3 --- /dev/null +++ b/spec/fixtures/ci_pipenv/bin/compile @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack, and tests that the environment is +# configured as expected for buildpacks that run after the Python buildpack. + +set -euo pipefail + +BUILD_DIR="${1}" + +cd "${BUILD_DIR}" + +exec bin/print-env-vars.sh diff --git a/spec/fixtures/ci_pipenv/bin/detect b/spec/fixtures/ci_pipenv/bin/detect new file mode 100755 index 000000000..68cdcc4a2 --- /dev/null +++ b/spec/fixtures/ci_pipenv/bin/detect @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack. + +set -euo pipefail + +echo "Inline" diff --git a/spec/fixtures/ci_pipenv/bin/post_compile b/spec/fixtures/ci_pipenv/bin/post_compile new file mode 100644 index 000000000..15362f6b1 --- /dev/null +++ b/spec/fixtures/ci_pipenv/bin/post_compile @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +exec bin/print-env-vars.sh diff --git a/spec/fixtures/ci_pipenv/bin/print-env-vars.sh b/spec/fixtures/ci_pipenv/bin/print-env-vars.sh new file mode 100755 index 000000000..9e0bebe6b --- /dev/null +++ b/spec/fixtures/ci_pipenv/bin/print-env-vars.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +printenv | sort | grep -vE '^(_|BUILDPACK_LOG_FILE|BUILD_DIR|CACHE_DIR|CI_NODE_.+|DYNO|ENV_DIR|HEROKU_TEST_RUN_.+|HOME|OLDPWD|PORT|PWD|SHLVL|STACK|TERM)=' diff --git a/spec/fixtures/ci_requirements/app.json b/spec/fixtures/ci_requirements/app.json index 99d1d237f..59617c169 100644 --- a/spec/fixtures/ci_requirements/app.json +++ b/spec/fixtures/ci_requirements/app.json @@ -2,7 +2,7 @@ "environments": { "test": { "scripts": { - "test": "pytest --version" + "test": "./bin/print-env-vars.sh && pytest --version" } } } diff --git a/spec/fixtures/ci_requirements/bin/compile b/spec/fixtures/ci_requirements/bin/compile new file mode 100755 index 000000000..fcb7055e3 --- /dev/null +++ b/spec/fixtures/ci_requirements/bin/compile @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack, and tests that the environment is +# configured as expected for buildpacks that run after the Python buildpack. + +set -euo pipefail + +BUILD_DIR="${1}" + +cd "${BUILD_DIR}" + +exec bin/print-env-vars.sh diff --git a/spec/fixtures/ci_requirements/bin/detect b/spec/fixtures/ci_requirements/bin/detect new file mode 100755 index 000000000..68cdcc4a2 --- /dev/null +++ b/spec/fixtures/ci_requirements/bin/detect @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack. + +set -euo pipefail + +echo "Inline" diff --git a/spec/fixtures/ci_requirements/bin/post_compile b/spec/fixtures/ci_requirements/bin/post_compile new file mode 100644 index 000000000..15362f6b1 --- /dev/null +++ b/spec/fixtures/ci_requirements/bin/post_compile @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +exec bin/print-env-vars.sh diff --git a/spec/fixtures/ci_requirements/bin/print-env-vars.sh b/spec/fixtures/ci_requirements/bin/print-env-vars.sh new file mode 100755 index 000000000..9e0bebe6b --- /dev/null +++ b/spec/fixtures/ci_requirements/bin/print-env-vars.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +printenv | sort | grep -vE '^(_|BUILDPACK_LOG_FILE|BUILD_DIR|CACHE_DIR|CI_NODE_.+|DYNO|ENV_DIR|HEROKU_TEST_RUN_.+|HOME|OLDPWD|PORT|PWD|SHLVL|STACK|TERM)=' diff --git a/spec/fixtures/ci_requirements/requirements-test.txt b/spec/fixtures/ci_requirements/requirements-test.txt index e079f8a60..40543aaba 100644 --- a/spec/fixtures/ci_requirements/requirements-test.txt +++ b/spec/fixtures/ci_requirements/requirements-test.txt @@ -1 +1 @@ -pytest +pytest==8.3.3 diff --git a/spec/fixtures/ci_requirements/requirements.txt b/spec/fixtures/ci_requirements/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/ci_requirements/requirements.txt +++ b/spec/fixtures/ci_requirements/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/hooks/bin/post_compile b/spec/fixtures/hooks/bin/post_compile index 373d33889..4bd8ed0e8 100644 --- a/spec/fixtures/hooks/bin/post_compile +++ b/spec/fixtures/hooks/bin/post_compile @@ -2,5 +2,6 @@ set -euo pipefail -echo 'post_compile ran with env vars:' -printenv | cut -d '=' -f 1 | sort +echo '~ post_compile ran with env vars:' +bin/print-env-vars.sh +echo '~ post_compile complete' diff --git a/spec/fixtures/hooks/bin/pre_compile b/spec/fixtures/hooks/bin/pre_compile index 40d1a402e..9f3737bef 100644 --- a/spec/fixtures/hooks/bin/pre_compile +++ b/spec/fixtures/hooks/bin/pre_compile @@ -2,5 +2,6 @@ set -euo pipefail -echo 'pre_compile ran with env vars:' -printenv | cut -d '=' -f 1 | sort +echo '~ pre_compile ran with env vars:' +bin/print-env-vars.sh +echo '~ pre_compile complete' diff --git a/spec/fixtures/hooks/bin/print-env-vars.sh b/spec/fixtures/hooks/bin/print-env-vars.sh new file mode 100755 index 000000000..dbd598a54 --- /dev/null +++ b/spec/fixtures/hooks/bin/print-env-vars.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +printenv | sort \ + | grep -vE '^(_|BUILDPACK_LOG_FILE|DYNO|OLDPWD|REQUEST_ID|SHLVL)=' \ + | sed --regexp-extended \ + --expression 's#(=/tmp/build_)[^:/]+#\1#' \ + --expression 's#^(ENV_DIR=/tmp/).*#\1...#' \ + --expression 's#^(SOURCE_VERSION=).*#\1...#' diff --git a/spec/fixtures/pipenv_editable/bin/compile b/spec/fixtures/pipenv_editable/bin/compile index c506ad6b7..df17e9401 100755 --- a/spec/fixtures/pipenv_editable/bin/compile +++ b/spec/fixtures/pipenv_editable/bin/compile @@ -9,4 +9,4 @@ BUILD_DIR="${1}" cd "${BUILD_DIR}" -exec bin/test-entrypoints +exec bin/test-entrypoints.sh diff --git a/spec/fixtures/pipenv_editable/bin/post_compile b/spec/fixtures/pipenv_editable/bin/post_compile index 460dc2de7..6e77d159a 100755 --- a/spec/fixtures/pipenv_editable/bin/post_compile +++ b/spec/fixtures/pipenv_editable/bin/post_compile @@ -2,4 +2,4 @@ set -euo pipefail -exec bin/test-entrypoints +exec bin/test-entrypoints.sh diff --git a/spec/fixtures/pipenv_editable/bin/test-entrypoints b/spec/fixtures/pipenv_editable/bin/test-entrypoints.sh similarity index 100% rename from spec/fixtures/pipenv_editable/bin/test-entrypoints rename to spec/fixtures/pipenv_editable/bin/test-entrypoints.sh diff --git a/spec/fixtures/pipenv_python_version_unspecified/Pipfile b/spec/fixtures/pipenv_python_version_unspecified/Pipfile index 67803f45b..496fa1558 100644 --- a/spec/fixtures/pipenv_python_version_unspecified/Pipfile +++ b/spec/fixtures/pipenv_python_version_unspecified/Pipfile @@ -4,6 +4,6 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] diff --git a/spec/fixtures/pipenv_python_version_unspecified/Pipfile.lock b/spec/fixtures/pipenv_python_version_unspecified/Pipfile.lock index 4c426c2ae..e744fd450 100644 --- a/spec/fixtures/pipenv_python_version_unspecified/Pipfile.lock +++ b/spec/fixtures/pipenv_python_version_unspecified/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a95c318e4395270fbd1e0f52dd8f12185db4c3258d4e03dd80de54b7c7aad8b1" + "sha256": "beb76460a63ef2f29eec7b281a3c7114d442db105096d7472b4b72a7504df8fe" }, "pipfile-spec": 6, "requires": {}, @@ -14,14 +14,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/pipenv_python_version_unspecified/bin/compile b/spec/fixtures/pipenv_python_version_unspecified/bin/compile new file mode 100755 index 000000000..db00975d6 --- /dev/null +++ b/spec/fixtures/pipenv_python_version_unspecified/bin/compile @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack, and tests that the environment is +# configured as expected for buildpacks that run after the Python buildpack. + +set -euo pipefail + +printenv | sort | grep -vE '^(_|BUILDPACK_LOG_FILE|DYNO|HOME|PWD|REQUEST_ID|SHLVL|SOURCE_VERSION|STACK)=' +echo + +python -c 'import pprint, sys; pprint.pp(sys.path)' +echo + +# TODO: Investigate why 'pipenv graph' doesn't work here. +# TODO: Remove --disable-pip-version-check in favour of exporting PIP_DISABLE_PIP_VERSION_CHECK +pip list --disable-pip-version-check +echo + +python -c 'import typing_extensions; print(typing_extensions)' diff --git a/spec/fixtures/pipenv_python_version_unspecified/bin/detect b/spec/fixtures/pipenv_python_version_unspecified/bin/detect new file mode 100755 index 000000000..68cdcc4a2 --- /dev/null +++ b/spec/fixtures/pipenv_python_version_unspecified/bin/detect @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack. + +set -euo pipefail + +echo "Inline" diff --git a/spec/fixtures/pipenv_python_version_unspecified/setup.py b/spec/fixtures/pipenv_python_version_unspecified/setup.py new file mode 100644 index 000000000..de90e907e --- /dev/null +++ b/spec/fixtures/pipenv_python_version_unspecified/setup.py @@ -0,0 +1,9 @@ +# This file is here to confirm we don't try and create the fallback requirements +# file containing '-e .' when using Pipenv. + +from setuptools import setup + +setup( + name='test', + install_requires=['six'], +) diff --git a/spec/fixtures/python_3.10_outdated/runtime.txt b/spec/fixtures/python_3.10_outdated/runtime.txt index a5da7cc4d..fadb07024 100644 --- a/spec/fixtures/python_3.10_outdated/runtime.txt +++ b/spec/fixtures/python_3.10_outdated/runtime.txt @@ -1 +1 @@ -python-3.10.5 +python-3.10.0 diff --git a/spec/fixtures/python_3.8_outdated/runtime.txt b/spec/fixtures/python_3.8_outdated/runtime.txt index d9a16ae1e..73b1cf81d 100644 --- a/spec/fixtures/python_3.8_outdated/runtime.txt +++ b/spec/fixtures/python_3.8_outdated/runtime.txt @@ -1 +1 @@ -python-3.8.12 +python-3.8.0 diff --git a/spec/fixtures/python_3.9_outdated/runtime.txt b/spec/fixtures/python_3.9_outdated/runtime.txt index 540296197..f72c5111f 100644 --- a/spec/fixtures/python_3.9_outdated/runtime.txt +++ b/spec/fixtures/python_3.9_outdated/runtime.txt @@ -1 +1 @@ -python-3.9.12 +python-3.9.0 diff --git a/spec/fixtures/requirements_basic/bin/compile b/spec/fixtures/requirements_basic/bin/compile new file mode 100755 index 000000000..68d243e7b --- /dev/null +++ b/spec/fixtures/requirements_basic/bin/compile @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack, and tests that the environment is +# configured as expected for buildpacks that run after the Python buildpack. + +set -euo pipefail + +printenv | sort | grep -vE '^(_|BUILDPACK_LOG_FILE|DYNO|HOME|PWD|REQUEST_ID|SHLVL|SOURCE_VERSION|STACK)=' +echo + +python -c 'import pprint, sys; pprint.pp(sys.path)' +echo + +# TODO: Remove --disable-pip-version-check in favour of exporting PIP_DISABLE_PIP_VERSION_CHECK +pip list --disable-pip-version-check +echo + +python -c 'import typing_extensions; print(typing_extensions)' diff --git a/spec/fixtures/requirements_basic/bin/detect b/spec/fixtures/requirements_basic/bin/detect new file mode 100755 index 000000000..68cdcc4a2 --- /dev/null +++ b/spec/fixtures/requirements_basic/bin/detect @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# This file is run by the inline buildpack. + +set -euo pipefail + +echo "Inline" diff --git a/spec/fixtures/requirements_basic/requirements.txt b/spec/fixtures/requirements_basic/requirements.txt new file mode 100644 index 000000000..eec3a2223 --- /dev/null +++ b/spec/fixtures/requirements_basic/requirements.txt @@ -0,0 +1,2 @@ +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/requirements_editable/bin/compile b/spec/fixtures/requirements_editable/bin/compile index c506ad6b7..df17e9401 100755 --- a/spec/fixtures/requirements_editable/bin/compile +++ b/spec/fixtures/requirements_editable/bin/compile @@ -9,4 +9,4 @@ BUILD_DIR="${1}" cd "${BUILD_DIR}" -exec bin/test-entrypoints +exec bin/test-entrypoints.sh diff --git a/spec/fixtures/requirements_editable/bin/post_compile b/spec/fixtures/requirements_editable/bin/post_compile index 460dc2de7..6e77d159a 100755 --- a/spec/fixtures/requirements_editable/bin/post_compile +++ b/spec/fixtures/requirements_editable/bin/post_compile @@ -2,4 +2,4 @@ set -euo pipefail -exec bin/test-entrypoints +exec bin/test-entrypoints.sh diff --git a/spec/fixtures/requirements_editable/bin/test-entrypoints b/spec/fixtures/requirements_editable/bin/test-entrypoints.sh similarity index 100% rename from spec/fixtures/requirements_editable/bin/test-entrypoints rename to spec/fixtures/requirements_editable/bin/test-entrypoints.sh diff --git a/spec/fixtures/requirements_git/requirements.txt b/spec/fixtures/requirements_git/requirements.txt deleted file mode 100644 index 81e9dbfe0..000000000 --- a/spec/fixtures/requirements_git/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# This relies upon the VCS binaries from the stack image. -git+https://github.com/certifi/python-certifi diff --git a/spec/fixtures/requirements_invalid/requirements.txt b/spec/fixtures/requirements_invalid/requirements.txt new file mode 100644 index 000000000..db42b7ee6 --- /dev/null +++ b/spec/fixtures/requirements_invalid/requirements.txt @@ -0,0 +1 @@ +an-invalid-requirement! diff --git a/spec/hatchet/ci_spec.rb b/spec/hatchet/ci_spec.rb index eb0b2c5db..ad8053abf 100644 --- a/spec/hatchet/ci_spec.rb +++ b/spec/hatchet/ci_spec.rb @@ -3,46 +3,163 @@ require_relative '../spec_helper' RSpec.describe 'Heroku CI' do - it 'installs both normal and test dependencies and uses cache on subsequent runs' do - Hatchet::Runner.new('spec/fixtures/ci_requirements', allow_failure: true).run_ci do |test_run| - expect(test_run.output).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) - -----> Python app detected - -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} - To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes - -----> Installing python-#{DEFAULT_PYTHON_VERSION} - -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - -----> Installing SQLite3 - -----> Installing requirements with pip - .* - Successfully installed urllib3-.* - -----> Installing test dependencies... - .* - Successfully installed .* pytest-.* - -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. - -----> No test-setup command provided. Skipping. - -----> Running test command `pytest --version`... - pytest .* - -----> test command `pytest --version` completed successfully - REGEX - - test_run.run_again - - expect(test_run.output).to match(Regexp.new(<<~REGEX)) - -----> Python app detected - -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} - To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes - -----> No change in requirements detected, installing from cache - -----> Using cached install of python-#{DEFAULT_PYTHON_VERSION} - -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - -----> Installing SQLite3 - -----> Installing requirements with pip - -----> Installing test dependencies... - -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. - -----> No test-setup command provided. Skipping. - -----> Running test command `pytest --version`... - pytest .* - -----> test command `pytest --version` completed successfully - REGEX + let(:buildpacks) { [:default, 'heroku-community/inline'] } + + context 'when using pip' do + let(:app) { Hatchet::Runner.new('spec/fixtures/ci_requirements', buildpacks:) } + + it 'installs both normal and test dependencies and uses cache on subsequent runs' do + app.run_ci do |test_run| + expect(test_run.output).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + -----> Python app detected + -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} + To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Installing python-#{DEFAULT_PYTHON_VERSION} + -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + -----> Installing SQLite3 + -----> Installing requirements with pip + .* + Successfully installed typing-extensions-4.12.2 + -----> Installing test dependencies... + .* + Successfully installed .* pytest-8.3.3 + -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. + -----> Running post-compile hook + CI=true + CPLUS_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + C_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + DISABLE_COLLECTSTATIC=1 + INSTALL_TEST=1 + LANG=en_US.UTF-8 + LC_ALL=C.UTF-8 + LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + PATH=/app/.heroku/python/bin:/app/.heroku/vendor/bin::/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ + PIP_NO_PYTHON_VERSION_WARNING=1 + PKG_CONFIG_PATH=/app/.heroku/vendor/lib/pkg-config:/app/.heroku/python/lib/pkg-config: + PYTHONUNBUFFERED=1 + -----> Inline app detected + LANG=en_US.UTF-8 + LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + PATH=/app/.heroku/python/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ + PYTHONHASHSEED=random + PYTHONHOME=/app/.heroku/python + PYTHONPATH=/app + PYTHONUNBUFFERED=true + -----> No test-setup command provided. Skipping. + -----> Running test command `./bin/print-env-vars.sh && pytest --version`... + CI=true + DYNO_RAM=2560 + FORWARDED_ALLOW_IPS=\\* + GUNICORN_CMD_ARGS=--access-logfile - + LANG=en_US.UTF-8 + LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + PATH=/app/.heroku/python/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/:/app/.sprettur/bin/ + PYTHONHASHSEED=random + PYTHONHOME=/app/.heroku/python + PYTHONPATH=/app + PYTHONUNBUFFERED=true + WEB_CONCURRENCY=5 + pytest 8.3.3 + -----> test command `./bin/print-env-vars.sh && pytest --version` completed successfully + REGEX + + test_run.run_again + + expect(test_run.output).to include(<<~OUTPUT) + -----> Python app detected + -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} + To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> No change in requirements detected, installing from cache + -----> Using cached install of python-#{DEFAULT_PYTHON_VERSION} + -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + -----> Installing SQLite3 + -----> Installing requirements with pip + -----> Installing test dependencies... + -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. + -----> Running post-compile hook + OUTPUT + end + end + end + + context 'when using Pipenv' do + let(:app) { Hatchet::Runner.new('spec/fixtures/ci_pipenv', buildpacks:) } + + it 'installs both normal and test dependencies and uses cache on subsequent runs' do + app.run_ci do |test_run| + expect(test_run.output).to match(Regexp.new(<<~REGEX)) + -----> Python app detected + -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} + To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Installing python-#{DEFAULT_PYTHON_VERSION} + -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + -----> Installing test dependencies + Installing dependencies from Pipfile.lock \\(.+\\)... + Installing dependencies from Pipfile.lock \\(.+\\)... + -----> Installing SQLite3 + -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. + -----> Running post-compile hook + CI=true + CPLUS_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + C_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + DISABLE_COLLECTSTATIC=1 + INSTALL_TEST=1 + LANG=en_US.UTF-8 + LC_ALL=C.UTF-8 + LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + PATH=/app/.heroku/python/bin:/app/.heroku/vendor/bin::/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ + PIP_NO_PYTHON_VERSION_WARNING=1 + PKG_CONFIG_PATH=/app/.heroku/vendor/lib/pkg-config:/app/.heroku/python/lib/pkg-config: + PYTHONUNBUFFERED=1 + SKIP_PIP_INSTALL=1 + -----> Inline app detected + LANG=en_US.UTF-8 + LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + PATH=/app/.heroku/python/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ + PYTHONHASHSEED=random + PYTHONHOME=/app/.heroku/python + PYTHONPATH=/app + PYTHONUNBUFFERED=true + -----> No test-setup command provided. Skipping. + -----> Running test command `./bin/print-env-vars.sh && pytest --version`... + CI=true + DYNO_RAM=2560 + FORWARDED_ALLOW_IPS=\\* + GUNICORN_CMD_ARGS=--access-logfile - + LANG=en_US.UTF-8 + LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + PATH=/app/.heroku/python/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/:/app/.sprettur/bin/ + PYTHONHASHSEED=random + PYTHONHOME=/app/.heroku/python + PYTHONPATH=/app + PYTHONUNBUFFERED=true + WEB_CONCURRENCY=5 + pytest 8.3.3 + -----> test command `./bin/print-env-vars.sh && pytest --version` completed successfully + REGEX + + test_run.run_again + + expect(test_run.output).to match(Regexp.new(<<~REGEX)) + -----> Python app detected + -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} + To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Using cached install of python-#{DEFAULT_PYTHON_VERSION} + -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + -----> Installing test dependencies + Installing dependencies from Pipfile.lock \\(.+\\)... + Installing dependencies from Pipfile.lock \\(.+\\)... + -----> Installing SQLite3 + -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. + -----> Running post-compile hook + REGEX + end end end end diff --git a/spec/hatchet/django_spec.rb b/spec/hatchet/django_spec.rb index 865d5b833..bd7feb1a9 100644 --- a/spec/hatchet/django_spec.rb +++ b/spec/hatchet/django_spec.rb @@ -97,10 +97,19 @@ expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) remote: -----> \\$ python manage.py collectstatic --noinput remote: Traceback \\(most recent call last\\): - remote: .* + remote: .+ remote: ModuleNotFoundError: No module named 'gettingstarted' remote: remote: ! Error while running '\\$ python manage.py collectstatic --noinput'. + remote: See traceback above for details. + remote: + remote: You may need to update application code to resolve this error. + remote: Or, you can disable collectstatic for this application: + remote: + remote: \\$ heroku config:set DISABLE_COLLECTSTATIC=1 + remote: + remote: https://devcenter.heroku.com/articles/django-assets + remote: ! Push rejected, failed to compile Python app. REGEX end end diff --git a/spec/hatchet/hooks_spec.rb b/spec/hatchet/hooks_spec.rb index dd114a76b..6d66adf54 100644 --- a/spec/hatchet/hooks_spec.rb +++ b/spec/hatchet/hooks_spec.rb @@ -3,49 +3,62 @@ require_relative '../spec_helper' RSpec.describe 'Compile hooks' do - context 'when an app has bin/pre_compile and bin/post_compile scripts' do + # TODO: Run this on Heroku-22 too, once it has also migrated to the new build infrastructure. + # (Currently the test fails on the old infrastructure due to subtle differences in system PATH elements.) + context 'when an app has bin/pre_compile and bin/post_compile scripts', stacks: %w[heroku-20 heroku-24] do let(:app) { Hatchet::Runner.new('spec/fixtures/hooks', config: { 'SOME_APP_CONFIG_VAR' => '1' }) } it 'runs the hooks with the correct environment' do - expected_env_vars = %w[ - _ - BUILD_DIR - BUILDPACK_LOG_FILE - CACHE_DIR - C_INCLUDE_PATH - CPLUS_INCLUDE_PATH - DYNO - ENV_DIR - HOME - LANG - LD_LIBRARY_PATH - LIBRARY_PATH - OLDPWD - PATH - PIP_NO_PYTHON_VERSION_WARNING - PKG_CONFIG_PATH - PWD - PYTHONUNBUFFERED - REQUEST_ID - SHLVL - SOME_APP_CONFIG_VAR - SOURCE_VERSION - STACK - ] - app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + output = clean_output(app.output) + + expect(output).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Running pre-compile hook - remote: pre_compile ran with env vars: - remote: #{expected_env_vars.join("\nremote: ")} - remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} - remote: .* + remote: ~ pre_compile ran with env vars: + remote: BUILD_DIR=/tmp/build_ + remote: CACHE_DIR=/tmp/codon/tmp/cache + remote: C_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + remote: CPLUS_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + remote: ENV_DIR=/tmp/... + remote: HOME=/app + remote: LANG=en_US.UTF-8 + remote: LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: PATH=/app/.heroku/python/bin:/app/.heroku/vendor/bin::/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + remote: PIP_NO_PYTHON_VERSION_WARNING=1 + remote: PKG_CONFIG_PATH=/app/.heroku/vendor/lib/pkg-config:/app/.heroku/python/lib/pkg-config: + remote: PWD=/tmp/build_ + remote: PYTHONUNBUFFERED=1 + remote: SOME_APP_CONFIG_VAR=1 + remote: SOURCE_VERSION=... + remote: STACK=#{app.stack} + remote: ~ pre_compile complete + OUTPUT + + expect(output).to include(<<~OUTPUT) remote: -----> Installing requirements with pip remote: -----> Running post-compile hook - remote: post_compile ran with env vars: - remote: #{expected_env_vars.join("\nremote: ")} - REGEX + remote: ~ post_compile ran with env vars: + remote: BUILD_DIR=/tmp/build_ + remote: CACHE_DIR=/tmp/codon/tmp/cache + remote: C_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + remote: CPLUS_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include: + remote: ENV_DIR=/tmp/... + remote: HOME=/app + remote: LANG=en_US.UTF-8 + remote: LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: PATH=/app/.heroku/python/bin:/app/.heroku/vendor/bin::/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + remote: PIP_NO_PYTHON_VERSION_WARNING=1 + remote: PKG_CONFIG_PATH=/app/.heroku/vendor/lib/pkg-config:/app/.heroku/python/lib/pkg-config: + remote: PWD=/tmp/build_ + remote: PYTHONUNBUFFERED=1 + remote: SOME_APP_CONFIG_VAR=1 + remote: SOURCE_VERSION=... + remote: STACK=#{app.stack} + remote: ~ post_compile complete + OUTPUT end end end diff --git a/spec/hatchet/package_manager_spec.rb b/spec/hatchet/package_manager_spec.rb index 10cff59b4..d247e1deb 100644 --- a/spec/hatchet/package_manager_spec.rb +++ b/spec/hatchet/package_manager_spec.rb @@ -35,6 +35,7 @@ remote: ! https://devcenter.heroku.com/articles/getting-started-with-python remote: ! https://devcenter.heroku.com/articles/python-support remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end diff --git a/spec/hatchet/pip_spec.rb b/spec/hatchet/pip_spec.rb index 704a65c4b..5351975e0 100644 --- a/spec/hatchet/pip_spec.rb +++ b/spec/hatchet/pip_spec.rb @@ -12,12 +12,15 @@ end RSpec.describe 'Pip support' do - context 'when requirements.txt is unchanged since the last build' do - let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } + # TODO: Run this on Heroku-22 too, once it has also migrated to the new build infrastructure. + # (Currently the test fails on the old infrastructure due to subtle differences in system PATH elements.) + context 'when requirements.txt is unchanged since the last build', stacks: %w[heroku-20 heroku-24] do + let(:buildpacks) { [:default, 'heroku-community/inline'] } + let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_basic', buildpacks:) } it 're-uses packages from the cache' do app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes @@ -25,12 +28,37 @@ remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 remote: -----> Installing requirements with pip - remote: Collecting urllib3 \\(from -r requirements.txt \\(line 1\\)\\) - remote: Downloading urllib3-.* - remote: Downloading urllib3-.* - remote: Installing collected packages: urllib3 - remote: Successfully installed urllib3-.* - REGEX + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) + remote: Downloading typing_extensions-4.12.2-py3-none-any.whl.metadata (3.0 kB) + remote: Downloading typing_extensions-4.12.2-py3-none-any.whl (37 kB) + remote: Installing collected packages: typing-extensions + remote: Successfully installed typing-extensions-4.12.2 + remote: -----> Inline app detected + remote: LANG=en_US.UTF-8 + remote: LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: PATH=/app/.heroku/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + remote: PYTHONHASHSEED=random + remote: PYTHONHOME=/app/.heroku/python + remote: PYTHONPATH=/app + remote: PYTHONUNBUFFERED=true + remote: + remote: ['', + remote: '/app', + remote: '/app/.heroku/python/lib/python312.zip', + remote: '/app/.heroku/python/lib/python3.12', + remote: '/app/.heroku/python/lib/python3.12/lib-dynload', + remote: '/app/.heroku/python/lib/python3.12/site-packages'] + remote: + remote: Package Version + remote: ----------------- ------- + remote: pip #{PIP_VERSION} + remote: setuptools #{SETUPTOOLS_VERSION} + remote: typing_extensions 4.12.2 + remote: wheel #{WHEEL_VERSION} + remote: + remote: + OUTPUT app.commit! app.push! expect(clean_output(app.output)).to include(<<~OUTPUT) @@ -42,21 +70,21 @@ remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 remote: -----> Installing requirements with pip - remote: -----> Discovering process types + remote: -----> Inline app detected OUTPUT end end end context 'when requirements.txt has changed since the last build' do - let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } + let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_basic') } it 'clears the cache before installing the packages again' do app.deploy do |app| File.write('requirements.txt', 'six', mode: 'a') app.commit! app.push! - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes @@ -65,32 +93,50 @@ remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 remote: -----> Installing requirements with pip - remote: Collecting urllib3 \\(from -r requirements.txt \\(line 1\\)\\) - remote: Downloading urllib3-.* - remote: Collecting six \\(from -r requirements.txt \\(line 2\\)\\) - remote: Downloading six-.* - remote: Downloading urllib3-.* - remote: Downloading six-.* - remote: Installing collected packages: urllib3, six - remote: Successfully installed six-.* urllib3-.* - REGEX + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) + remote: Downloading typing_extensions-4.12.2-py3-none-any.whl.metadata (3.0 kB) + remote: Collecting six (from -r requirements.txt (line 3)) + remote: Downloading six-1.16.0-py2.py3-none-any.whl.metadata (1.8 kB) + remote: Downloading typing_extensions-4.12.2-py3-none-any.whl (37 kB) + remote: Downloading six-1.16.0-py2.py3-none-any.whl (11 kB) + remote: Installing collected packages: typing-extensions, six + remote: Successfully installed six-1.16.0 typing-extensions-4.12.2 + OUTPUT end end end - context 'when requirements.txt contains popular compiled packages' do - let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_compiled') } + context 'when the package manager has changed from Pipenv to pip since the last build' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified') } - include_examples 'installs successfully using pip' + # TODO: Fix this case so the cache is actually cleared. + it 'clears the cache before installing with pip' do + app.deploy do |app| + FileUtils.rm(['Pipfile', 'Pipfile.lock']) + FileUtils.cp(FIXTURE_DIR.join('requirements_basic/requirements.txt'), '.') + app.commit! + app.push! + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Python app detected + remote: -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} + remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using cached install of python-#{DEFAULT_PYTHON_VERSION} + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + remote: -----> Installing SQLite3 + remote: -----> Installing requirements with pip + remote: -----> Discovering process types + REGEX + end + end end - context 'when requirements.txt contains Git requirements URLs' do - let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_git') } + context 'when requirements.txt contains popular compiled packages' do + let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_compiled') } include_examples 'installs successfully using pip' end - context 'when requirements.txt contains editable requirements' do + context 'when requirements.txt contains editable requirements (both VCS and local package)' do let(:buildpacks) { [:default, 'heroku-community/inline'] } let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_editable', buildpacks:) } @@ -120,7 +166,7 @@ REGEX # Test rewritten paths work at runtime. - expect(app.run('bin/test-entrypoints')).to include(<<~OUTPUT) + expect(app.run('bin/test-entrypoints.sh')).to include(<<~OUTPUT) easy-install.pth:/app/.heroku/src/gunicorn easy-install.pth:/app/packages/local_package_setup_py __editable___local_package_pyproject_toml_0_0_1_finder.py:/app/packages/local_package_pyproject_toml/local_package_pyproject_toml'} @@ -166,8 +212,21 @@ it 'installs packages from setup.py' do app.deploy do |app| - expect(app.output).to include('Running setup.py develop for test') - expect(app.output).to include('Successfully installed six') + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + remote: -----> Python app detected + remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} + remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + remote: -----> Installing SQLite3 + remote: -----> Installing requirements with pip + remote: Obtaining file:///tmp/build_.* \\(from -r requirements.txt \\(line 1\\)\\) + remote: Preparing metadata \\(setup.py\\): started + remote: Preparing metadata \\(setup.py\\): finished with status 'done' + remote: .+ + remote: Installing collected packages: six, test + remote: Running setup.py develop for test + REGEX end end end @@ -186,6 +245,20 @@ end end + context 'when requirements.txt contains an invalid requirement' do + let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_invalid', allow_failure: true) } + + it 'aborts the build and displays the pip error' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Installing requirements with pip + remote: ERROR: Invalid requirement: 'an-invalid-requirement!' (from line 1 of requirements.txt) + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end + context 'when requirements.txt contains GDAL but the GDAL C++ library is missing' do let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_gdal', allow_failure: true) } @@ -195,6 +268,7 @@ remote: ! Hello! Package installation failed since the GDAL library was not found. remote: ! For GDAL, GEOS and PROJ support, use the Geo buildpack alongside the Python buildpack: remote: ! https://github.com/heroku/heroku-geo-buildpack + remote: ! Push rejected, failed to compile Python app. OUTPUT end end diff --git a/spec/hatchet/pipenv_spec.rb b/spec/hatchet/pipenv_spec.rb index ff5b5ba1c..6c0e2fa97 100644 --- a/spec/hatchet/pipenv_spec.rb +++ b/spec/hatchet/pipenv_spec.rb @@ -30,6 +30,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -63,10 +64,14 @@ end context 'with a Pipfile.lock but no Python version specified' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified') } + let(:buildpacks) { [:default, 'heroku-community/inline'] } + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified', buildpacks:) } - it 'builds with the default Python version' do + # TODO: Run this on Heroku-22 too, once it has also migrated to the new build infrastructure. + # (Currently the test fails on the old infrastructure due to subtle differences in system PATH elements.) + it 'builds with the default Python version', stacks: %w[heroku-20 heroku-24] do app.deploy do |app| + # TODO: We should not be leaking the Pipenv installation into the app environment. expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} @@ -76,6 +81,37 @@ remote: -----> Installing dependencies with Pipenv #{PIPENV_VERSION} remote: Installing dependencies from Pipfile.lock \\(.+\\)... remote: -----> Installing SQLite3 + remote: -----> Inline app detected + remote: LANG=en_US.UTF-8 + remote: LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib: + remote: PATH=/app/.heroku/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + remote: PYTHONHASHSEED=random + remote: PYTHONHOME=/app/.heroku/python + remote: PYTHONPATH=/app + remote: PYTHONUNBUFFERED=true + remote: + remote: \\['', + remote: '/app', + remote: '/app/.heroku/python/lib/python312.zip', + remote: '/app/.heroku/python/lib/python3.12', + remote: '/app/.heroku/python/lib/python3.12/lib-dynload', + remote: '/app/.heroku/python/lib/python3.12/site-packages'\\] + remote: + remote: Package Version + remote: ----------------- --------- + remote: certifi .+ + remote: distlib .+ + remote: filelock .+ + remote: pip #{PIP_VERSION} + remote: pipenv #{PIPENV_VERSION} + remote: platformdirs .+ + remote: setuptools #{SETUPTOOLS_VERSION} + remote: typing_extensions 4.12.2 + remote: virtualenv .+ + remote: wheel #{WHEEL_VERSION} + remote: + remote: \\ REGEX end end @@ -101,6 +137,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -126,6 +163,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -207,7 +245,7 @@ let(:allow_failure) { false } let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_full_version', allow_failure:) } - it 'builds with the outdated Python version specified' do + it 'builds with the outdated Python version specified and displays a deprecation warning' do app.deploy do |app| expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected @@ -241,6 +279,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -260,6 +299,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -305,6 +345,32 @@ end end + context 'when the package manager has changed from pip to Pipenv since the last build' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } + + # TODO: Fix this case so the cache is actually cleared. + it 'clears the cache before installing with Pipenv' do + app.deploy do |app| + FileUtils.rm('requirements.txt') + FileUtils.cp(FIXTURE_DIR.join('pipenv_python_version_unspecified/Pipfile'), '.') + FileUtils.cp(FIXTURE_DIR.join('pipenv_python_version_unspecified/Pipfile.lock'), '.') + app.commit! + app.push! + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Python app detected + remote: -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} + remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using cached install of python-#{DEFAULT_PYTHON_VERSION} + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + remote: -----> Installing dependencies with Pipenv #{PIPENV_VERSION} + remote: Installing dependencies from Pipfile.lock \\(.+\\)... + remote: -----> Installing SQLite3 + remote: -----> Discovering process types + REGEX + end + end + end + context 'when there is both a Pipfile.lock and a requirements.txt' do let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_and_requirements_txt') } @@ -338,6 +404,7 @@ remote: Your Pipfile.lock \\(.+\\) is out of date. Expected: \\(.+\\). remote: .+ remote: ERROR:: Aborting deploy + remote: ! Push rejected, failed to compile Python app. REGEX end end @@ -373,7 +440,7 @@ REGEX # Test rewritten paths work at runtime. - expect(app.run('bin/test-entrypoints')).to include(<<~OUTPUT) + expect(app.run('bin/test-entrypoints.sh')).to include(<<~OUTPUT) easy-install.pth:/app/.heroku/src/gunicorn easy-install.pth:/app/packages/local_package_setup_py __editable___local_package_pyproject_toml_0_0_1_finder.py:/app/packages/local_package_pyproject_toml/local_package_pyproject_toml'} diff --git a/spec/hatchet/python_update_warning_spec.rb b/spec/hatchet/python_update_warning_spec.rb index 6c7f66d62..ab5c25316 100644 --- a/spec/hatchet/python_update_warning_spec.rb +++ b/spec/hatchet/python_update_warning_spec.rb @@ -30,13 +30,17 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end end +# Note: We use the oldest patch releases (ie the '.0' releases) since we also want to test against +# the oldest Python versions available to users. This is particularly important given that older +# patch releases will bundle older pip, and the buildpack uses that pip during bootstrapping. RSpec.describe 'Python update warnings' do - context 'with a runtime.txt containing python-3.8.12' do + context 'with a runtime.txt containing python-3.8.0' do let(:allow_failure) { false } let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.8_outdated', allow_failure:) } @@ -60,7 +64,7 @@ remote: ! A Python security update is available! Upgrade as soon as possible to: python-#{LATEST_PYTHON_3_8} remote: ! See: https://devcenter.heroku.com/articles/python-runtimes remote: ! - remote: -----> Installing python-3.8.12 + remote: -----> Installing python-3.8.0 REGEX end end @@ -70,31 +74,31 @@ let(:allow_failure) { true } # We only support Python 3.8 on Heroku-20 and older. - include_examples 'aborts the build without showing an update warning', '3.8.12' + include_examples 'aborts the build without showing an update warning', '3.8.0' end end - context 'with a runtime.txt containing python-3.9.12' do + context 'with a runtime.txt containing python-3.9.0' do let(:allow_failure) { false } let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.9_outdated', allow_failure:) } context 'when using Heroku-22 or older', stacks: %w[heroku-20 heroku-22] do - include_examples 'warns there is a Python update available', '3.9.12', LATEST_PYTHON_3_9 + include_examples 'warns there is a Python update available', '3.9.0', LATEST_PYTHON_3_9 end context 'when using Heroku-24', stacks: %w[heroku-24] do let(:allow_failure) { true } # We only support Python 3.9 on Heroku-22 and older. - include_examples 'aborts the build without showing an update warning', '3.9.12' + include_examples 'aborts the build without showing an update warning', '3.9.0' end end - context 'with a runtime.txt containing python-3.10.5' do + context 'with a runtime.txt containing python-3.10.0' do let(:allow_failure) { false } let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.10_outdated', allow_failure:) } - include_examples 'warns there is a Python update available', '3.10.5', LATEST_PYTHON_3_10 + include_examples 'warns there is a Python update available', '3.10.0', LATEST_PYTHON_3_10 end context 'with a runtime.txt containing python-3.11.0' do diff --git a/spec/hatchet/python_version_spec.rb b/spec/hatchet/python_version_spec.rb index f4d96c376..a324089ba 100644 --- a/spec/hatchet/python_version_spec.rb +++ b/spec/hatchet/python_version_spec.rb @@ -31,6 +31,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -102,6 +103,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end @@ -127,6 +129,7 @@ remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: ! + remote: ! Push rejected, failed to compile Python app. OUTPUT end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6d72bc8ee..93e3b2bdb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,6 +7,8 @@ require 'rspec/retry' require 'hatchet' +FIXTURE_DIR = Pathname.new(__FILE__).parent.join('fixtures') + LATEST_PYTHON_3_8 = '3.8.20' LATEST_PYTHON_3_9 = '3.9.20' LATEST_PYTHON_3_10 = '3.10.15'