mirror of https://github.com/pypa/pip
Merge pull request #8118 from ilanschnell/yaml_updates
This commit is contained in:
commit
cbfbc29b63
|
@ -29,7 +29,7 @@ def generate_yaml_tests(directory):
|
|||
"""
|
||||
Generate yaml test cases from the yaml files in the given directory
|
||||
"""
|
||||
for yml_file in directory.glob("*/*.yml"):
|
||||
for yml_file in directory.glob("*.yml"):
|
||||
data = yaml.safe_load(yml_file.read_text())
|
||||
assert "cases" in data, "A fixture needs cases to be used in testing"
|
||||
|
||||
|
@ -40,18 +40,23 @@ def generate_yaml_tests(directory):
|
|||
base = data.get("base", {})
|
||||
cases = data["cases"]
|
||||
|
||||
for i, case_template in enumerate(cases):
|
||||
case = base.copy()
|
||||
case.update(case_template)
|
||||
for resolver in 'old', 'new':
|
||||
for i, case_template in enumerate(cases):
|
||||
case = base.copy()
|
||||
case.update(case_template)
|
||||
|
||||
case[":name:"] = base_name
|
||||
if len(cases) > 1:
|
||||
case[":name:"] += "-" + str(i)
|
||||
case[":name:"] = base_name
|
||||
if len(cases) > 1:
|
||||
case[":name:"] += "-" + str(i)
|
||||
case[":name:"] += "*" + resolver
|
||||
case[":resolver:"] = resolver
|
||||
|
||||
if case.pop("skip", False):
|
||||
case = pytest.param(case, marks=pytest.mark.xfail)
|
||||
skip = case.pop("skip", False)
|
||||
assert skip in [False, True, 'old', 'new']
|
||||
if skip is True or skip == resolver:
|
||||
case = pytest.param(case, marks=pytest.mark.xfail)
|
||||
|
||||
yield case
|
||||
yield case
|
||||
|
||||
|
||||
def id_func(param):
|
||||
|
@ -92,60 +97,44 @@ def convert_to_dict(string):
|
|||
return retval
|
||||
|
||||
|
||||
def handle_request(script, action, requirement, options):
|
||||
assert isinstance(requirement, str), (
|
||||
"Need install requirement to be a string only"
|
||||
)
|
||||
def handle_request(script, action, requirement, options, new_resolver=False):
|
||||
if action == 'install':
|
||||
args = ['install', "--no-index", "--find-links",
|
||||
path_to_url(script.scratch_path)]
|
||||
args = ['install']
|
||||
if new_resolver:
|
||||
args.append("--unstable-feature=resolver")
|
||||
args.extend(["--no-index", "--find-links",
|
||||
path_to_url(script.scratch_path)])
|
||||
elif action == 'uninstall':
|
||||
args = ['uninstall', '--yes']
|
||||
else:
|
||||
raise "Did not excpet action: {!r}".format(action)
|
||||
args.append(requirement)
|
||||
|
||||
if isinstance(requirement, str):
|
||||
args.append(requirement)
|
||||
elif isinstance(requirement, list):
|
||||
args.extend(requirement)
|
||||
else:
|
||||
raise "requirement neither str nor list {!r}".format(requirement)
|
||||
|
||||
args.extend(options)
|
||||
args.append("--verbose")
|
||||
|
||||
result = script.pip(*args,
|
||||
allow_stderr_error=True,
|
||||
allow_stderr_warning=True)
|
||||
allow_stderr_warning=True,
|
||||
allow_error=True)
|
||||
|
||||
retval = {
|
||||
"_result_object": result,
|
||||
}
|
||||
if result.returncode == 0:
|
||||
# Check which packages got installed
|
||||
retval["state"] = []
|
||||
# Check which packages got installed
|
||||
state = []
|
||||
for path in os.listdir(script.site_packages_path):
|
||||
if path.endswith(".dist-info"):
|
||||
name, version = (
|
||||
os.path.basename(path)[:-len(".dist-info")]
|
||||
).rsplit("-", 1)
|
||||
# TODO: information about extras.
|
||||
state.append(" ".join((name, version)))
|
||||
|
||||
for path in os.listdir(script.site_packages_path):
|
||||
if path.endswith(".dist-info"):
|
||||
name, version = (
|
||||
os.path.basename(path)[:-len(".dist-info")]
|
||||
).rsplit("-", 1)
|
||||
|
||||
# TODO: information about extras.
|
||||
|
||||
retval["state"].append(" ".join((name, version)))
|
||||
|
||||
retval["state"].sort()
|
||||
|
||||
elif "conflicting" in result.stderr.lower():
|
||||
retval["conflicting"] = []
|
||||
|
||||
message = result.stderr.rsplit("\n", 1)[-1]
|
||||
|
||||
# XXX: There might be a better way than parsing the message
|
||||
for match in re.finditer(message, _conflict_finder_pat):
|
||||
di = match.groupdict()
|
||||
retval["conflicting"].append(
|
||||
{
|
||||
"required_by": "{} {}".format(di["name"], di["version"]),
|
||||
"selector": di["selector"]
|
||||
}
|
||||
)
|
||||
|
||||
return retval
|
||||
return {"result": result, "state": sorted(state)}
|
||||
|
||||
|
||||
@pytest.mark.yaml
|
||||
|
@ -184,7 +173,26 @@ def test_yaml_based(script, case):
|
|||
# Perform the requested action
|
||||
effect = handle_request(script, action,
|
||||
request[action],
|
||||
request.get('options', '').split())
|
||||
request.get('options', '').split(),
|
||||
case[':resolver:'] == 'new')
|
||||
|
||||
assert effect['state'] == (response['state'] or []), \
|
||||
str(effect["_result_object"])
|
||||
if 0: # for analyzing output easier
|
||||
with open(DATA_DIR.parent / "yaml" /
|
||||
case[':name:'].replace('*', '-'), 'w') as fo:
|
||||
result = effect['result']
|
||||
fo.write("=== RETURNCODE = %d\n" % result.returncode)
|
||||
fo.write("=== STDERR ===:\n%s\n" % result.stderr)
|
||||
|
||||
if 'state' in response:
|
||||
assert effect['state'] == (response['state'] or []), \
|
||||
str(effect["result"])
|
||||
|
||||
error = False
|
||||
if 'conflicting' in response:
|
||||
error = True
|
||||
|
||||
if error:
|
||||
if case[":resolver:"] == 'old':
|
||||
assert effect["result"].returncode == 0, str(effect["result"])
|
||||
elif case[":resolver:"] == 'new':
|
||||
assert effect["result"].returncode == 1, str(effect["result"])
|
||||
|
|
|
@ -533,6 +533,10 @@ class PipTestEnvironment(TestFileEnvironment):
|
|||
`allow_stderr_warning` since warnings are weaker than errors.
|
||||
:param allow_stderr_warning: whether a logged warning (or
|
||||
deprecation message) is allowed in stderr.
|
||||
:param allow_error: if True (default is False) does not raise
|
||||
exception when the command exit value is non-zero. Implies
|
||||
expect_error, but in contrast to expect_error will not assert
|
||||
that the exit value is zero.
|
||||
:param expect_error: if False (the default), asserts that the command
|
||||
exits with 0. Otherwise, asserts that the command exits with a
|
||||
non-zero exit code. Passing True also implies allow_stderr_error
|
||||
|
@ -553,10 +557,14 @@ class PipTestEnvironment(TestFileEnvironment):
|
|||
# Partial fix for ScriptTest.run using `shell=True` on Windows.
|
||||
args = [str(a).replace('^', '^^').replace('&', '^&') for a in args]
|
||||
|
||||
# Remove `allow_stderr_error` and `allow_stderr_warning` before
|
||||
# calling run() because PipTestEnvironment doesn't support them.
|
||||
# Remove `allow_stderr_error`, `allow_stderr_warning` and
|
||||
# `allow_error` before calling run() because PipTestEnvironment
|
||||
# doesn't support them.
|
||||
allow_stderr_error = kw.pop('allow_stderr_error', None)
|
||||
allow_stderr_warning = kw.pop('allow_stderr_warning', None)
|
||||
allow_error = kw.pop('allow_error', None)
|
||||
if allow_error:
|
||||
kw['expect_error'] = True
|
||||
|
||||
# Propagate default values.
|
||||
expect_error = kw.get('expect_error')
|
||||
|
@ -596,7 +604,7 @@ class PipTestEnvironment(TestFileEnvironment):
|
|||
kw['expect_stderr'] = True
|
||||
result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw)
|
||||
|
||||
if expect_error:
|
||||
if expect_error and not allow_error:
|
||||
if result.returncode == 0:
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("Script passed unexpectedly.")
|
||||
|
|
|
@ -1,5 +1,31 @@
|
|||
# Fixtures
|
||||
|
||||
This directory contains fixtures for testing pip's resolver. The fixtures are written as yml files, with a convenient format that allows for specifying a custom index for temporary use.
|
||||
This directory contains fixtures for testing pip's resolver.
|
||||
The fixtures are written as `.yml` files, with a convenient format
|
||||
that allows for specifying a custom index for temporary use.
|
||||
|
||||
The `.yml` files are organized in the following way. A `base` section
|
||||
which ...
|
||||
|
||||
The linter is very useful for initally checking `.yml` files, e.g.:
|
||||
|
||||
$ python linter.py -v simple.yml
|
||||
|
||||
To run only the yaml tests, use (from the root of the source tree):
|
||||
|
||||
$ tox -e py38 -- -m yaml -vv
|
||||
|
||||
Or, in order to avoid collecting all the test cases:
|
||||
|
||||
$ tox -e py38 -- tests/functional/test_yaml.py
|
||||
|
||||
Or, only a specific test:
|
||||
|
||||
$ tox -e py38 -- tests/functional/test_yaml.py -k simple
|
||||
|
||||
Or, just a specific test case:
|
||||
|
||||
$ tox -e py38 -- tests/functional/test_yaml.py -k simple-0
|
||||
|
||||
|
||||
<!-- TODO: Add a good description of the format and how it can be used. -->
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# Pradyun's backtracking example
|
||||
base:
|
||||
available:
|
||||
- A 1.0.0; depends B == 1.0.0
|
||||
- A 2.0.0; depends B == 2.0.0, C == 1.0.0
|
||||
- A 3.0.0; depends B == 3.0.0, C == 2.0.0
|
||||
- A 4.0.0; depends B == 4.0.0, C == 3.0.0
|
||||
- A 5.0.0; depends B == 5.0.0, C == 4.0.0
|
||||
- A 6.0.0; depends B == 6.0.0, C == 5.0.0
|
||||
- A 7.0.0; depends B == 7.0.0, C == 6.0.0
|
||||
- A 8.0.0; depends B == 8.0.0, C == 7.0.0
|
||||
|
||||
- B 1.0.0; depends C == 1.0.0
|
||||
- B 2.0.0; depends C == 2.0.0
|
||||
- B 3.0.0; depends C == 3.0.0
|
||||
- B 4.0.0; depends C == 4.0.0
|
||||
- B 5.0.0; depends C == 5.0.0
|
||||
- B 6.0.0; depends C == 6.0.0
|
||||
- B 7.0.0; depends C == 7.0.0
|
||||
- B 8.0.0; depends C == 8.0.0
|
||||
|
||||
- C 1.0.0
|
||||
- C 2.0.0
|
||||
- C 3.0.0
|
||||
- C 4.0.0
|
||||
- C 5.0.0
|
||||
- C 6.0.0
|
||||
- C 7.0.0
|
||||
- C 8.0.0
|
||||
|
||||
cases:
|
||||
-
|
||||
request:
|
||||
- install: A
|
||||
response:
|
||||
- state:
|
||||
- A 1.0.0
|
||||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
skip: old
|
|
@ -16,6 +16,7 @@ cases:
|
|||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
- D 1.0.0
|
||||
skip: new
|
||||
-
|
||||
request:
|
||||
- install: B
|
||||
|
@ -25,6 +26,7 @@ cases:
|
|||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
- D 1.0.0
|
||||
skip: new
|
||||
-
|
||||
request:
|
||||
- install: C
|
||||
|
@ -34,6 +36,7 @@ cases:
|
|||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
- D 1.0.0
|
||||
skip: new
|
||||
-
|
||||
request:
|
||||
- install: D
|
||||
|
@ -43,3 +46,4 @@ cases:
|
|||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
- D 1.0.0
|
||||
skip: new
|
|
@ -39,4 +39,4 @@ cases:
|
|||
- D 1.0.0
|
||||
- E 1.0.0
|
||||
- F 1.0.0
|
||||
skip: true
|
||||
skip: old
|
|
@ -0,0 +1,92 @@
|
|||
import sys
|
||||
from pprint import pprint
|
||||
|
||||
import yaml
|
||||
|
||||
sys.path.insert(0, '../../src')
|
||||
sys.path.insert(0, '../..')
|
||||
|
||||
|
||||
def check_dict(d, required=None, optional=None):
|
||||
assert isinstance(d, dict)
|
||||
if required is None:
|
||||
required = []
|
||||
if optional is None:
|
||||
optional = []
|
||||
for key in required:
|
||||
if key not in d:
|
||||
sys.exit("key %r is required" % key)
|
||||
allowed_keys = set(required)
|
||||
allowed_keys.update(optional)
|
||||
for key in d.keys():
|
||||
if key not in allowed_keys:
|
||||
sys.exit("key %r is not allowed. Allowed keys are: %r" %
|
||||
(key, allowed_keys))
|
||||
|
||||
|
||||
def lint_case(case, verbose=False):
|
||||
from tests.functional.test_yaml import convert_to_dict
|
||||
|
||||
if verbose:
|
||||
print("--- linting case ---")
|
||||
pprint(case)
|
||||
|
||||
check_dict(case, optional=['available', 'request', 'response', 'skip'])
|
||||
available = case.get("available", [])
|
||||
requests = case.get("request", [])
|
||||
responses = case.get("response", [])
|
||||
assert isinstance(available, list)
|
||||
assert isinstance(requests, list)
|
||||
assert isinstance(responses, list)
|
||||
assert len(requests) == len(responses)
|
||||
|
||||
for package in available:
|
||||
if isinstance(package, str):
|
||||
package = convert_to_dict(package)
|
||||
if verbose:
|
||||
pprint(package)
|
||||
check_dict(package,
|
||||
required=['name', 'version'],
|
||||
optional=['depends', 'extras'])
|
||||
|
||||
for request, response in zip(requests, responses):
|
||||
check_dict(request, optional=['install', 'uninstall', 'options'])
|
||||
check_dict(response, optional=['state', 'conflicting'])
|
||||
assert len(response) == 1
|
||||
assert isinstance(response.get('state') or [], list)
|
||||
|
||||
|
||||
def lint_yml(yml_file, verbose=False):
|
||||
if verbose:
|
||||
print("=== linting: %s ===" % yml_file)
|
||||
assert yml_file.endswith(".yml")
|
||||
with open(yml_file) as fi:
|
||||
data = yaml.safe_load(fi)
|
||||
if verbose:
|
||||
pprint(data)
|
||||
|
||||
check_dict(data, required=['cases'], optional=['base'])
|
||||
base = data.get("base", {})
|
||||
cases = data["cases"]
|
||||
for i, case_template in enumerate(cases):
|
||||
case = base.copy()
|
||||
case.update(case_template)
|
||||
lint_case(case, verbose)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from optparse import OptionParser
|
||||
|
||||
p = OptionParser(usage="usage: %prog [options] FILE ...",
|
||||
description="linter for pip's yaml test FILE(s)")
|
||||
|
||||
p.add_option('-v', '--verbose',
|
||||
action="store_true")
|
||||
|
||||
opts, args = p.parse_args()
|
||||
|
||||
if len(args) < 1:
|
||||
p.error('at least one argument required, try -h')
|
||||
|
||||
for yml_file in args:
|
||||
lint_yml(yml_file, opts.verbose)
|
|
@ -0,0 +1,44 @@
|
|||
# https://medium.com/knerd/the-nine-circles-of-python-dependency-hell-481d53e3e025
|
||||
# Circle 4: Overlapping transitive dependencies
|
||||
base:
|
||||
available:
|
||||
- myapp 0.2.4; depends fussy, capridous
|
||||
- name: fussy
|
||||
version: 3.8.0
|
||||
depends: ['requests >=1.2.0,<3']
|
||||
- name: capridous
|
||||
version: 1.1.0
|
||||
depends: ['requests >=1.0.3,<2']
|
||||
- requests 1.0.1
|
||||
- requests 1.0.3
|
||||
- requests 1.1.0
|
||||
- requests 1.2.0
|
||||
- requests 1.3.0
|
||||
- requests 2.1.0
|
||||
- requests 3.2.0
|
||||
|
||||
cases:
|
||||
-
|
||||
request:
|
||||
- install: myapp
|
||||
response:
|
||||
- state:
|
||||
- capridous 1.1.0
|
||||
- fussy 3.8.0
|
||||
- myapp 0.2.4
|
||||
- requests 1.3.0
|
||||
skip: old
|
||||
-
|
||||
request:
|
||||
- install: fussy
|
||||
response:
|
||||
- state:
|
||||
- fussy 3.8.0
|
||||
- requests 2.1.0
|
||||
-
|
||||
request:
|
||||
- install: capridous
|
||||
response:
|
||||
- state:
|
||||
- capridous 1.1.0
|
||||
- requests 1.3.0
|
|
@ -0,0 +1,37 @@
|
|||
# https://github.com/pypa/pip/issues/988#issuecomment-606967707
|
||||
base:
|
||||
available:
|
||||
- A 1.0.0; depends B >= 1.0.0, C >= 1.0.0
|
||||
- A 2.0.0; depends B >= 2.0.0, C >= 1.0.0
|
||||
- B 1.0.0; depends C >= 1.0.0
|
||||
- B 2.0.0; depends C >= 2.0.0
|
||||
- C 1.0.0
|
||||
- C 2.0.0
|
||||
|
||||
cases:
|
||||
-
|
||||
request:
|
||||
- install: C==1.0.0
|
||||
- install: B==1.0.0
|
||||
- install: A==1.0.0
|
||||
- install: A==2.0.0
|
||||
response:
|
||||
- state:
|
||||
- C 1.0.0
|
||||
- state:
|
||||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
- state:
|
||||
- A 1.0.0
|
||||
- B 1.0.0
|
||||
- C 1.0.0
|
||||
- state:
|
||||
- A 2.0.0
|
||||
- B 2.0.0
|
||||
- C 2.0.0
|
||||
# for the last install (A==2.0.0) the old resolver gives
|
||||
# - A 2.0.0
|
||||
# - B 2.0.0
|
||||
# - C 1.0.0
|
||||
# but because B 2.0.0 depends on C >=2.0.0 this is wrong
|
||||
skip: old
|
|
@ -0,0 +1,24 @@
|
|||
# see: https://github.com/python-poetry/poetry/issues/2298
|
||||
base:
|
||||
available:
|
||||
- poetry 1.0.5; depends zappa == 0.51.0, sphinx == 3.0.1
|
||||
- zappa 0.51.0; depends boto3
|
||||
- sphinx 3.0.1; depends docutils
|
||||
- boto3 1.4.5; depends botocore ~=1.5.0
|
||||
- botocore 1.5.92; depends docutils <0.16
|
||||
- docutils 0.16.0
|
||||
- docutils 0.15.0
|
||||
|
||||
cases:
|
||||
-
|
||||
request:
|
||||
- install: poetry
|
||||
response:
|
||||
- state:
|
||||
- boto3 1.4.5
|
||||
- botocore 1.5.92
|
||||
- docutils 0.15.0
|
||||
- poetry 1.0.5
|
||||
- sphinx 3.0.1
|
||||
- zappa 0.51.0
|
||||
skip: old
|
|
@ -38,3 +38,10 @@ cases:
|
|||
response:
|
||||
- state:
|
||||
- base 0.1.0
|
||||
-
|
||||
request:
|
||||
- install: ['dep', 'simple==0.1.0']
|
||||
response:
|
||||
- state:
|
||||
- dep 0.1.0
|
||||
- simple 0.1.0
|
|
@ -0,0 +1,24 @@
|
|||
base:
|
||||
available:
|
||||
- a 0.1.0
|
||||
- b 0.2.0
|
||||
- c 0.3.0
|
||||
|
||||
cases:
|
||||
-
|
||||
request:
|
||||
- install: ['a', 'b']
|
||||
- install: c
|
||||
- uninstall: ['b', 'c']
|
||||
- uninstall: a
|
||||
response:
|
||||
- state:
|
||||
- a 0.1.0
|
||||
- b 0.2.0
|
||||
- state:
|
||||
- a 0.1.0
|
||||
- b 0.2.0
|
||||
- c 0.3.0
|
||||
- state:
|
||||
- a 0.1.0
|
||||
- state: null
|
Loading…
Reference in New Issue