5
5
6
6
import inspect
7
7
import os
8
- import random
9
8
import re
10
9
import shutil
11
10
import socket
12
- import string
13
11
import sys
14
12
import textwrap
15
13
import warnings
16
14
from contextlib import closing , contextmanager
17
15
from pathlib import Path
18
- from subprocess import PIPE , Popen , check_call
19
- from threading import Thread
20
16
from types import ModuleType , TracebackType
21
- from typing import IO , TYPE_CHECKING , Any , Callable , Iterator , Sequence , cast
17
+ from typing import TYPE_CHECKING , Any , Callable , Iterator , Sequence , cast
22
18
from unittest .mock import MagicMock
23
19
24
20
import pytest
30
26
from _pytest .monkeypatch import MonkeyPatch
31
27
from _pytest .python import Function
32
28
from _pytest .tmpdir import TempPathFactory
29
+ from devpi_process import IndexServer
33
30
from pytest_mock import MockerFixture
34
- from virtualenv .discovery .py_info import PythonInfo
35
- from virtualenv .info import IS_WIN , fs_supports_symlink
31
+ from virtualenv .info import fs_supports_symlink
36
32
37
33
import tox .run
38
34
from tox .config .sets import EnvConfigSet
@@ -467,26 +463,6 @@ def is_integration(test_item: Function) -> bool:
467
463
items .sort (key = is_integration )
468
464
469
465
470
- class Index :
471
- def __init__ (self , base_url : str , name : str , client_cmd_base : list [str ]) -> None :
472
- self ._client_cmd_base = client_cmd_base
473
- self ._server_url = base_url
474
- self .name = name
475
-
476
- @property
477
- def url (self ) -> str :
478
- return f"{ self ._server_url } /{ self .name } /+simple"
479
-
480
- def upload (self , files : Sequence [Path ]) -> None :
481
- check_call (self ._client_cmd_base + ["upload" , "--index" , self .name ] + [str (i ) for i in files ])
482
-
483
- def __repr__ (self ) -> str :
484
- return f"{ self .__class__ .__name__ } (url={ self .url } )" # pragma: no cover
485
-
486
- def use (self , monkeypatch : MonkeyPatch ) -> None :
487
- enable_pypi_server (monkeypatch , self .url )
488
-
489
-
490
466
def enable_pypi_server (monkeypatch : MonkeyPatch , url : str | None ) -> None :
491
467
if url is None : # pragma: no cover # only one of the branches can be hit depending on env
492
468
monkeypatch .delenv ("PIP_INDEX_URL" , raising = False )
@@ -496,109 +472,6 @@ def enable_pypi_server(monkeypatch: MonkeyPatch, url: str | None) -> None:
496
472
monkeypatch .setenv ("PIP_TIMEOUT" , str (2 ))
497
473
498
474
499
- def _find_free_port () -> int :
500
- with closing (socket .socket (socket .AF_INET , socket .SOCK_STREAM )) as socket_handler :
501
- socket_handler .bind (("" , 0 ))
502
- return cast (int , socket_handler .getsockname ()[1 ])
503
-
504
-
505
- class IndexServer :
506
- def __init__ (self , path : Path ) -> None :
507
- self .path = path
508
-
509
- self .host , self .port = "localhost" , _find_free_port ()
510
- self ._passwd = "" .join (random .choices (string .ascii_letters , k = 8 ))
511
-
512
- def _exe (name : str ) -> str :
513
- return str (Path (scripts_dir ) / f"{ name } { '.exe' if IS_WIN else '' } " )
514
-
515
- scripts_dir = PythonInfo .current ().sysconfig_path ("scripts" )
516
- self ._init : str = _exe ("devpi-init" )
517
- self ._server : str = _exe ("devpi-server" )
518
- self ._client : str = _exe ("devpi" )
519
-
520
- self ._server_dir = self .path / "server"
521
- self ._client_dir = self .path / "client"
522
- self ._indexes : dict [str , Index ] = {}
523
- self ._process : Popen [str ] | None = None
524
- self ._has_use = False
525
- self ._stdout_drain : Thread | None = None
526
-
527
- def __enter__ (self ) -> IndexServer :
528
- self ._create_and_start_server ()
529
- self ._setup_client ()
530
- return self
531
-
532
- def _create_and_start_server (self ) -> None :
533
- self ._server_dir .mkdir (exist_ok = True )
534
- server_at = str (self ._server_dir )
535
- # 1. create the server
536
- cmd = [self ._init , "--serverdir" , server_at ]
537
- cmd .extend (("--no-root-pypi" , "--role" , "standalone" , "--root-passwd" , self ._passwd ))
538
- check_call (cmd , stdout = PIPE , stderr = PIPE )
539
- # 2. start the server
540
- cmd = [self ._server , "--serverdir" , server_at , "--port" , str (self .port ), "--offline-mode" ]
541
- self ._process = Popen (cmd , stdout = PIPE , universal_newlines = True )
542
- stdout = self ._drain_stdout ()
543
- for line in stdout : # pragma: no branch # will always loop at least once
544
- if "serving at url" in line :
545
-
546
- def _keep_draining () -> None :
547
- for _ in stdout :
548
- pass
549
-
550
- # important to keep draining the stdout, otherwise once the buffer is full Windows blocks the process
551
- self ._stdout_drain = Thread (target = _keep_draining , name = "tox-test-stdout-drain" )
552
- self ._stdout_drain .start ()
553
- break
554
-
555
- def _drain_stdout (self ) -> Iterator [str ]:
556
- process = cast ("Popen[str]" , self ._process )
557
- stdout = cast (IO [str ], process .stdout )
558
- while True :
559
- if process .poll () is not None : # pragma: no cover
560
- print (f"devpi server with pid { process .pid } at { self ._server_dir } died" )
561
- break
562
- yield stdout .readline ()
563
-
564
- def _setup_client (self ) -> None :
565
- """create a user on the server and authenticate it"""
566
- self ._client_dir .mkdir (exist_ok = True )
567
- base = ["--clientdir" , str (self ._client_dir )]
568
- check_call ([self ._client , "use" ] + base + [self .url ], stdout = PIPE , stderr = PIPE )
569
- check_call ([self ._client , "login" ] + base + ["root" , "--password" , self ._passwd ], stdout = PIPE , stderr = PIPE )
570
-
571
- def create_index (self , name : str , * args : str ) -> Index :
572
- if name in self ._indexes : # pragma: no cover
573
- raise ValueError (f"index { name } already exists" )
574
- base = [self ._client , "--clientdir" , str (self ._client_dir )]
575
- check_call (base + ["index" , "-c" , name , * args ], stdout = PIPE , stderr = PIPE )
576
- index = Index (f"{ self .url } /root" , name , base )
577
- if not self ._has_use :
578
- self ._has_use = True
579
- check_call (base + ["use" , f"root/{ name } " ], stdout = PIPE , stderr = PIPE )
580
- self ._indexes [name ] = index
581
- return index
582
-
583
- def __exit__ (
584
- self ,
585
- exc_type : type [BaseException ] | None , # noqa: U100
586
- exc_val : BaseException | None , # noqa: U100
587
- exc_tb : TracebackType | None , # noqa: U100
588
- ) -> None :
589
- if self ._process is not None : # pragma: no cover # defend against devpi startup fail
590
- self ._process .terminate ()
591
- if self ._stdout_drain is not None and self ._stdout_drain .is_alive (): # pragma: no cover # devpi startup fail
592
- self ._stdout_drain .join ()
593
-
594
- @property
595
- def url (self ) -> str :
596
- return f"http://{ self .host } :{ self .port } "
597
-
598
- def __repr__ (self ) -> str :
599
- return f"{ self .__class__ .__name__ } (url={ self .url } , indexes={ list (self ._indexes )} )" # pragma: no cover
600
-
601
-
602
475
@pytest .fixture (scope = "session" )
603
476
def pypi_server (tmp_path_factory : TempPathFactory ) -> Iterator [IndexServer ]:
604
477
# takes around 2.5s
@@ -610,7 +483,9 @@ def pypi_server(tmp_path_factory: TempPathFactory) -> Iterator[IndexServer]:
610
483
611
484
@pytest .fixture (scope = "session" )
612
485
def _invalid_index_fake_port () -> int : # noqa: PT005
613
- return _find_free_port ()
486
+ with closing (socket .socket (socket .AF_INET , socket .SOCK_STREAM )) as socket_handler :
487
+ socket_handler .bind (("" , 0 ))
488
+ return cast (int , socket_handler .getsockname ()[1 ])
614
489
615
490
616
491
@pytest .fixture (autouse = True )
@@ -654,7 +529,5 @@ def register_inline_plugin(mocker: MockerFixture, *args: Callable[..., Any]) ->
654
529
"ToxProject" ,
655
530
"ToxProjectCreator" ,
656
531
"check_os_environ" ,
657
- "IndexServer" ,
658
- "Index" ,
659
532
"register_inline_plugin" ,
660
533
)
0 commit comments