| 1 | ;;; GNU Guix --- Functional package management for GNU |
| 2 | ;;; Copyright © 2014 David Thompson <davet@gnu.org> |
| 3 | ;;; Copyright © 2016 Ricardo Wurmus <rekado@elephly.net> |
| 4 | ;;; Copyright © 2019 Maxim Cournoyer <maxim.cournoyer@gmail.com> |
| 5 | ;;; |
| 6 | ;;; This file is part of GNU Guix. |
| 7 | ;;; |
| 8 | ;;; GNU Guix is free software; you can redistribute it and/or modify it |
| 9 | ;;; under the terms of the GNU General Public License as published by |
| 10 | ;;; the Free Software Foundation; either version 3 of the License, or (at |
| 11 | ;;; your option) any later version. |
| 12 | ;;; |
| 13 | ;;; GNU Guix is distributed in the hope that it will be useful, but |
| 14 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | ;;; GNU General Public License for more details. |
| 17 | ;;; |
| 18 | ;;; You should have received a copy of the GNU General Public License |
| 19 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. |
| 20 | |
| 21 | (define-module (test-pypi) |
| 22 | #:use-module (guix import pypi) |
| 23 | #:use-module (guix base32) |
| 24 | #:use-module (guix memoization) |
| 25 | #:use-module (gcrypt hash) |
| 26 | #:use-module (guix tests) |
| 27 | #:use-module (guix build-system python) |
| 28 | #:use-module ((guix build utils) #:select (delete-file-recursively which mkdir-p)) |
| 29 | #:use-module (srfi srfi-64) |
| 30 | #:use-module (ice-9 match)) |
| 31 | |
| 32 | (define test-json |
| 33 | "{ |
| 34 | \"info\": { |
| 35 | \"version\": \"1.0.0\", |
| 36 | \"name\": \"foo\", |
| 37 | \"license\": \"GNU LGPL\", |
| 38 | \"summary\": \"summary\", |
| 39 | \"home_page\": \"http://example.com\", |
| 40 | \"classifiers\": [], |
| 41 | \"download_url\": \"\" |
| 42 | }, |
| 43 | \"urls\": [], |
| 44 | \"releases\": { |
| 45 | \"1.0.0\": [ |
| 46 | { |
| 47 | \"url\": \"https://example.com/foo-1.0.0.egg\", |
| 48 | \"packagetype\": \"bdist_egg\" |
| 49 | }, { |
| 50 | \"url\": \"https://example.com/foo-1.0.0.tar.gz\", |
| 51 | \"packagetype\": \"sdist\" |
| 52 | }, { |
| 53 | \"url\": \"https://example.com/foo-1.0.0-py2.py3-none-any.whl\", |
| 54 | \"packagetype\": \"bdist_wheel\" |
| 55 | } |
| 56 | ] |
| 57 | } |
| 58 | }") |
| 59 | |
| 60 | (define test-source-hash |
| 61 | "") |
| 62 | |
| 63 | (define test-specifications |
| 64 | '("Fizzy [foo, bar]" |
| 65 | "PickyThing<1.6,>1.9,!=1.9.6,<2.0a0,==2.4c1" |
| 66 | "SomethingWithMarker[foo]>1.0;python_version<\"2.7\"" |
| 67 | "requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < \"2.7\"" |
| 68 | "pip @ https://github.com/pypa/pip/archive/1.3.1.zip#\ |
| 69 | sha1=da9234ee9982d4bbb3c72346a6de940a148ea686")) |
| 70 | |
| 71 | (define test-requires.txt "\ |
| 72 | # A comment |
| 73 | # A comment after a space |
| 74 | foo ~= 3 |
| 75 | bar != 2 |
| 76 | |
| 77 | [test] |
| 78 | pytest (>=2.5.0) |
| 79 | ") |
| 80 | |
| 81 | ;; Beaker contains only optional dependencies. |
| 82 | (define test-requires.txt-beaker "\ |
| 83 | [crypto] |
| 84 | pycryptopp>=0.5.12 |
| 85 | |
| 86 | [cryptography] |
| 87 | cryptography |
| 88 | |
| 89 | [testsuite] |
| 90 | Mock |
| 91 | coverage |
| 92 | ") |
| 93 | |
| 94 | (define test-metadata "\ |
| 95 | Classifier: Programming Language :: Python :: 3.7 |
| 96 | Requires-Dist: baz ~= 3 |
| 97 | Requires-Dist: bar != 2 |
| 98 | Provides-Extra: test |
| 99 | Requires-Dist: pytest (>=2.5.0) ; extra == 'test' |
| 100 | ") |
| 101 | |
| 102 | (define test-metadata-with-extras " |
| 103 | Classifier: Programming Language :: Python :: 3.7 |
| 104 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* |
| 105 | Requires-Dist: wrapt (<2,>=1) |
| 106 | Requires-Dist: bar |
| 107 | |
| 108 | Provides-Extra: dev |
| 109 | Requires-Dist: tox ; extra == 'dev' |
| 110 | Requires-Dist: bumpversion (<1) ; extra == 'dev' |
| 111 | ") |
| 112 | |
| 113 | ;;; Provides-Extra can appear before Requires-Dist. |
| 114 | (define test-metadata-with-extras-jedi "\ |
| 115 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* |
| 116 | Provides-Extra: testing |
| 117 | Requires-Dist: parso (>=0.3.0) |
| 118 | Provides-Extra: testing |
| 119 | Requires-Dist: pytest (>=3.1.0); extra == 'testing' |
| 120 | ") |
| 121 | |
| 122 | \f |
| 123 | (test-begin "pypi") |
| 124 | |
| 125 | (test-equal "guix-package->pypi-name, old URL style" |
| 126 | "psutil" |
| 127 | (guix-package->pypi-name |
| 128 | (dummy-package "foo" |
| 129 | (source (dummy-origin |
| 130 | (uri |
| 131 | "https://pypi.org/packages/source/p/psutil/psutil-4.3.0.tar.gz")))))) |
| 132 | |
| 133 | (test-equal "guix-package->pypi-name, new URL style" |
| 134 | "certbot" |
| 135 | (guix-package->pypi-name |
| 136 | (dummy-package "foo" |
| 137 | (source (dummy-origin |
| 138 | (uri |
| 139 | "https://pypi.org/packages/a2/3b/4756e6a0ceb14e084042a2a65c615d68d25621c6fd446d0fc10d14c4ce7d/certbot-0.8.1.tar.gz")))))) |
| 140 | |
| 141 | (test-equal "guix-package->pypi-name, several URLs" |
| 142 | "cram" |
| 143 | (guix-package->pypi-name |
| 144 | (dummy-package "foo" |
| 145 | (source |
| 146 | (dummy-origin |
| 147 | (uri (list "https://bitheap.org/cram/cram-0.7.tar.gz" |
| 148 | (pypi-uri "cram" "0.7")))))))) |
| 149 | |
| 150 | (test-equal "specification->requirement-name" |
| 151 | '("Fizzy" "PickyThing" "SomethingWithMarker" "requests" "pip") |
| 152 | (map specification->requirement-name test-specifications)) |
| 153 | |
| 154 | (test-equal "parse-requires.txt" |
| 155 | (list '("foo" "bar") '("pytest")) |
| 156 | (mock ((ice-9 ports) call-with-input-file |
| 157 | call-with-input-string) |
| 158 | (parse-requires.txt test-requires.txt))) |
| 159 | |
| 160 | (test-equal "parse-requires.txt - Beaker" |
| 161 | (list '() '("Mock" "coverage")) |
| 162 | (mock ((ice-9 ports) call-with-input-file |
| 163 | call-with-input-string) |
| 164 | (parse-requires.txt test-requires.txt-beaker))) |
| 165 | |
| 166 | (test-equal "parse-wheel-metadata, with extras" |
| 167 | (list '("wrapt" "bar") '("tox" "bumpversion")) |
| 168 | (mock ((ice-9 ports) call-with-input-file |
| 169 | call-with-input-string) |
| 170 | (parse-wheel-metadata test-metadata-with-extras))) |
| 171 | |
| 172 | (test-equal "parse-wheel-metadata, with extras - Jedi" |
| 173 | (list '("parso") '("pytest")) |
| 174 | (mock ((ice-9 ports) call-with-input-file |
| 175 | call-with-input-string) |
| 176 | (parse-wheel-metadata test-metadata-with-extras-jedi))) |
| 177 | |
| 178 | (test-assert "pypi->guix-package, no wheel" |
| 179 | ;; Replace network resources with sample data. |
| 180 | (mock ((guix import utils) url-fetch |
| 181 | (lambda (url file-name) |
| 182 | (match url |
| 183 | ("https://example.com/foo-1.0.0.tar.gz" |
| 184 | (begin |
| 185 | ;; Unusual requires.txt location should still be found. |
| 186 | (mkdir-p "foo-1.0.0/src/bizarre.egg-info") |
| 187 | (with-output-to-file "foo-1.0.0/src/bizarre.egg-info/requires.txt" |
| 188 | (lambda () |
| 189 | (display test-requires.txt))) |
| 190 | (parameterize ((current-output-port (%make-void-port "rw+"))) |
| 191 | (system* "tar" "czvf" file-name "foo-1.0.0/")) |
| 192 | (delete-file-recursively "foo-1.0.0") |
| 193 | (set! test-source-hash |
| 194 | (call-with-input-file file-name port-sha256)))) |
| 195 | ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f) |
| 196 | (_ (error "Unexpected URL: " url))))) |
| 197 | (mock ((guix http-client) http-fetch |
| 198 | (lambda (url . rest) |
| 199 | (match url |
| 200 | ("https://pypi.org/pypi/foo/json" |
| 201 | (values (open-input-string test-json) |
| 202 | (string-length test-json))) |
| 203 | ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f) |
| 204 | (_ (error "Unexpected URL: " url))))) |
| 205 | (match (pypi->guix-package "foo") |
| 206 | (('package |
| 207 | ('name "python-foo") |
| 208 | ('version "1.0.0") |
| 209 | ('source ('origin |
| 210 | ('method 'url-fetch) |
| 211 | ('uri ('pypi-uri "foo" 'version)) |
| 212 | ('sha256 |
| 213 | ('base32 |
| 214 | (? string? hash))))) |
| 215 | ('build-system 'python-build-system) |
| 216 | ('propagated-inputs |
| 217 | ('quasiquote |
| 218 | (("python-bar" ('unquote 'python-bar)) |
| 219 | ("python-foo" ('unquote 'python-foo))))) |
| 220 | ('native-inputs |
| 221 | ('quasiquote |
| 222 | (("python-pytest" ('unquote 'python-pytest))))) |
| 223 | ('home-page "http://example.com") |
| 224 | ('synopsis "summary") |
| 225 | ('description "summary") |
| 226 | ('license 'license:lgpl2.0)) |
| 227 | (string=? (bytevector->nix-base32-string |
| 228 | test-source-hash) |
| 229 | hash)) |
| 230 | (x |
| 231 | (pk 'fail x #f)))))) |
| 232 | |
| 233 | (test-skip (if (which "zip") 0 1)) |
| 234 | (test-assert "pypi->guix-package, wheels" |
| 235 | ;; Replace network resources with sample data. |
| 236 | (mock ((guix import utils) url-fetch |
| 237 | (lambda (url file-name) |
| 238 | (match url |
| 239 | ("https://example.com/foo-1.0.0.tar.gz" |
| 240 | (begin |
| 241 | (mkdir-p "foo-1.0.0/foo.egg-info/") |
| 242 | (with-output-to-file "foo-1.0.0/foo.egg-info/requires.txt" |
| 243 | (lambda () |
| 244 | (display "wrong data to make sure we're testing wheels "))) |
| 245 | (parameterize ((current-output-port (%make-void-port "rw+"))) |
| 246 | (system* "tar" "czvf" file-name "foo-1.0.0/")) |
| 247 | (delete-file-recursively "foo-1.0.0") |
| 248 | (set! test-source-hash |
| 249 | (call-with-input-file file-name port-sha256)))) |
| 250 | ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" |
| 251 | (begin |
| 252 | (mkdir "foo-1.0.0.dist-info") |
| 253 | (with-output-to-file "foo-1.0.0.dist-info/METADATA" |
| 254 | (lambda () |
| 255 | (display test-metadata))) |
| 256 | (let ((zip-file (string-append file-name ".zip"))) |
| 257 | ;; zip always adds a "zip" extension to the file it creates, |
| 258 | ;; so we need to rename it. |
| 259 | (system* "zip" "-q" zip-file "foo-1.0.0.dist-info/METADATA") |
| 260 | (rename-file zip-file file-name)) |
| 261 | (delete-file-recursively "foo-1.0.0.dist-info"))) |
| 262 | (_ (error "Unexpected URL: " url))))) |
| 263 | (mock ((guix http-client) http-fetch |
| 264 | (lambda (url . rest) |
| 265 | (match url |
| 266 | ("https://pypi.org/pypi/foo/json" |
| 267 | (values (open-input-string test-json) |
| 268 | (string-length test-json))) |
| 269 | ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f) |
| 270 | (_ (error "Unexpected URL: " url))))) |
| 271 | ;; Not clearing the memoization cache here would mean returning the value |
| 272 | ;; computed in the previous test. |
| 273 | (invalidate-memoization! pypi->guix-package) |
| 274 | (match (pypi->guix-package "foo") |
| 275 | (('package |
| 276 | ('name "python-foo") |
| 277 | ('version "1.0.0") |
| 278 | ('source ('origin |
| 279 | ('method 'url-fetch) |
| 280 | ('uri ('pypi-uri "foo" 'version)) |
| 281 | ('sha256 |
| 282 | ('base32 |
| 283 | (? string? hash))))) |
| 284 | ('build-system 'python-build-system) |
| 285 | ('propagated-inputs |
| 286 | ('quasiquote |
| 287 | (("python-bar" ('unquote 'python-bar)) |
| 288 | ("python-baz" ('unquote 'python-baz))))) |
| 289 | ('native-inputs |
| 290 | ('quasiquote |
| 291 | (("python-pytest" ('unquote 'python-pytest))))) |
| 292 | ('home-page "http://example.com") |
| 293 | ('synopsis "summary") |
| 294 | ('description "summary") |
| 295 | ('license 'license:lgpl2.0)) |
| 296 | (string=? (bytevector->nix-base32-string |
| 297 | test-source-hash) |
| 298 | hash)) |
| 299 | (x |
| 300 | (pk 'fail x #f)))))) |
| 301 | |
| 302 | (test-assert "pypi->guix-package, no usable requirement file." |
| 303 | ;; Replace network resources with sample data. |
| 304 | (mock ((guix import utils) url-fetch |
| 305 | (lambda (url file-name) |
| 306 | (match url |
| 307 | ("https://example.com/foo-1.0.0.tar.gz" |
| 308 | (mkdir-p "foo-1.0.0/foo.egg-info/") |
| 309 | (parameterize ((current-output-port (%make-void-port "rw+"))) |
| 310 | (system* "tar" "czvf" file-name "foo-1.0.0/")) |
| 311 | (delete-file-recursively "foo-1.0.0") |
| 312 | (set! test-source-hash |
| 313 | (call-with-input-file file-name port-sha256))) |
| 314 | ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f) |
| 315 | (_ (error "Unexpected URL: " url))))) |
| 316 | (mock ((guix http-client) http-fetch |
| 317 | (lambda (url . rest) |
| 318 | (match url |
| 319 | ("https://pypi.org/pypi/foo/json" |
| 320 | (values (open-input-string test-json) |
| 321 | (string-length test-json))) |
| 322 | ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f) |
| 323 | (_ (error "Unexpected URL: " url))))) |
| 324 | ;; Not clearing the memoization cache here would mean returning the value |
| 325 | ;; computed in the previous test. |
| 326 | (invalidate-memoization! pypi->guix-package) |
| 327 | (match (pypi->guix-package "foo") |
| 328 | (('package |
| 329 | ('name "python-foo") |
| 330 | ('version "1.0.0") |
| 331 | ('source ('origin |
| 332 | ('method 'url-fetch) |
| 333 | ('uri ('pypi-uri "foo" 'version)) |
| 334 | ('sha256 |
| 335 | ('base32 |
| 336 | (? string? hash))))) |
| 337 | ('build-system 'python-build-system) |
| 338 | ('home-page "http://example.com") |
| 339 | ('synopsis "summary") |
| 340 | ('description "summary") |
| 341 | ('license 'license:lgpl2.0)) |
| 342 | (string=? (bytevector->nix-base32-string |
| 343 | test-source-hash) |
| 344 | hash)) |
| 345 | (x |
| 346 | (pk 'fail x #f)))))) |
| 347 | |
| 348 | (test-end "pypi") |