1
1
import logging
2
2
import os
3
+ import sys
4
+ from dataclasses import dataclass
3
5
from http .server import HTTPServer
4
6
from pathlib import Path
5
- from typing import Any , Iterator , List , Optional
7
+ from typing import Any , Iterator , List , Optional , Tuple
8
+ from unittest .mock import patch
6
9
from urllib .parse import urlparse
7
10
from urllib .request import getproxies
8
11
9
12
import pytest
10
13
from pip ._vendor import requests
11
14
12
15
from pip import __version__
13
- from pip ._internal .exceptions import DiagnosticPipError
16
+ from pip ._internal .exceptions import (
17
+ ConnectionFailedError ,
18
+ ConnectionTimeoutError ,
19
+ DiagnosticPipError ,
20
+ ProxyConnectionError ,
21
+ SSLVerificationError ,
22
+ )
14
23
from pip ._internal .models .link import Link
15
24
from pip ._internal .network .session import (
16
25
CI_ENVIRONMENT_VARIABLES ,
17
26
PipSession ,
18
27
user_agent ,
19
28
)
20
29
from pip ._internal .utils .logging import VERBOSE
30
+ from tests .lib .output import render_to_text
21
31
from tests .lib .server import InstantCloseHTTPHandler , server_running
22
32
23
33
34
+ def render_diagnostic_error (error : DiagnosticPipError ) -> Tuple [str , Optional [str ]]:
35
+ message = render_to_text (error .message ).rstrip ()
36
+ if error .context is None :
37
+ return (message , None )
38
+ return (message , render_to_text (error .context ).rstrip ())
39
+
40
+
41
+ @dataclass (frozen = True )
42
+ class Address :
43
+ host : str
44
+ port : int
45
+
46
+ @property
47
+ def url (self ) -> str :
48
+ return f"http://{ self .host } :{ self .port } /"
49
+
50
+
51
+ @pytest .fixture (scope = "module" )
52
+ def instant_close_server () -> Iterator [Address ]:
53
+ with HTTPServer (("127.0.0.1" , 0 ), InstantCloseHTTPHandler ) as server :
54
+ with server_running (server ):
55
+ yield Address (server .server_name , server .server_port )
56
+
57
+
24
58
def get_user_agent () -> str :
25
59
# These tests are testing the computation of the user agent, so we want to
26
60
# avoid reusing cached values.
@@ -321,14 +355,18 @@ def test_timeout(self, caplog: pytest.LogCaptureFixture) -> None:
321
355
"server didn't respond within 0.2 seconds, retrying 1 last time"
322
356
]
323
357
324
- def test_connection_aborted (self , caplog : pytest .LogCaptureFixture ) -> None :
325
- with HTTPServer (("localhost" , 0 ), InstantCloseHTTPHandler ) as server :
326
- with server_running (server ), PipSession (retries = 1 ) as session :
327
- with pytest .raises (DiagnosticPipError ):
328
- session .get (f"http://{ server .server_name } :{ server .server_port } /" )
329
- assert caplog .messages == [
330
- "the connection was closed unexpectedly, retrying 1 last time"
331
- ]
358
+ @pytest .mark .skipif (
359
+ sys .platform != "linux" , reason = "Only Linux raises the needed urllib3 error"
360
+ )
361
+ def test_connection_closed_by_peer (
362
+ self , caplog : pytest .LogCaptureFixture , instant_close_server : Address
363
+ ) -> None :
364
+ with PipSession (retries = 1 ) as session :
365
+ with pytest .raises (DiagnosticPipError ):
366
+ session .get (instant_close_server .url )
367
+ assert caplog .messages == [
368
+ "the connection was closed unexpectedly, retrying 1 last time"
369
+ ]
332
370
333
371
def test_proxy (self , caplog : pytest .LogCaptureFixture ) -> None :
334
372
with PipSession (retries = 1 ) as session :
@@ -345,3 +383,73 @@ def test_verbose(self, caplog: pytest.LogCaptureFixture) -> None:
345
383
warnings = [r .message for r in caplog .records if r .levelno == logging .WARNING ]
346
384
assert len (warnings ) == 1
347
385
assert not warnings [0 ].endswith ("retrying 1 last time" )
386
+
387
+
388
+ @pytest .mark .network
389
+ class TestConnectionErrors :
390
+ @pytest .fixture
391
+ def session (self ) -> Iterator [PipSession ]:
392
+ with PipSession () as session :
393
+ yield session
394
+
395
+ def test_non_existent_domain (self , session : PipSession ) -> None :
396
+ url = "https://404.example.com/"
397
+ with pytest .raises (ConnectionFailedError ) as e :
398
+ session .get (url )
399
+ message , _ = render_diagnostic_error (e .value )
400
+ assert message == f"Failed to connect to 404.example.com while fetching { url } "
401
+
402
+ @pytest .mark .skipif (
403
+ sys .platform != "linux" , reason = "Only Linux raises the needed urllib3 error"
404
+ )
405
+ def test_connection_closed_by_peer (
406
+ self , session : PipSession , instant_close_server : Address
407
+ ) -> None :
408
+ with pytest .raises (ConnectionFailedError ) as e :
409
+ session .get (instant_close_server .url )
410
+ message , context = render_diagnostic_error (e .value )
411
+ assert message == (
412
+ f"Failed to connect to { instant_close_server .host } "
413
+ f"while fetching { instant_close_server .url } "
414
+ )
415
+ assert context == (
416
+ "Details: the connection was closed without a reply from the server."
417
+ )
418
+
419
+ def test_timeout (self , session : PipSession ) -> None :
420
+ url = "https://httpstat.us/200?sleep=400"
421
+ with pytest .raises (ConnectionTimeoutError ) as e :
422
+ session .get (url , timeout = 0.2 )
423
+ message , context = render_diagnostic_error (e .value )
424
+ assert message == f"Unable to fetch { url } "
425
+ assert context is not None
426
+ assert context .startswith ("httpstat.us didn't respond within 0.2 seconds" )
427
+
428
+ def test_expired_ssl (self , session : PipSession ) -> None :
429
+ url = "https://expired.badssl.com/"
430
+ with pytest .raises (SSLVerificationError ) as e :
431
+ session .get (url )
432
+ message , _ = render_diagnostic_error (e .value )
433
+ assert message == (
434
+ "Failed to establish a secure connection to expired.badssl.com "
435
+ f"while fetching { url } "
436
+ )
437
+
438
+ @patch ("pip._internal.network.utils.has_tls" , lambda : False )
439
+ def test_missing_python_ssl_support (self , session : PipSession ) -> None :
440
+ # So, this demonstrates a potentially invalid assumption: a SSL error
441
+ # when SSL support is missing is assumed to be caused by that. Not ideal
442
+ # but unlikely to cause issues in practice.
443
+ with pytest .raises (SSLVerificationError ) as e :
444
+ session .get ("https://expired.badssl.com/" )
445
+ _ , context = render_diagnostic_error (e .value )
446
+ assert context == "The built-in ssl module is not available."
447
+
448
+ def test_broken_proxy (self , session : PipSession ) -> None :
449
+ url = "https://pypi.org/"
450
+ proxy = "https://404.example.com"
451
+ session .proxies = {"https" : proxy }
452
+ with pytest .raises (ProxyConnectionError ) as e :
453
+ session .get (url )
454
+ message , _ = render_diagnostic_error (e .value )
455
+ assert message == f"Failed to connect to proxy { proxy } :443 while fetching { url } "
0 commit comments