Merge pull request #8118 from ilanschnell/yaml_updates

This commit is contained in:
Pradyun Gedam 2020-05-04 19:30:50 +05:30 committed by GitHub
commit cbfbc29b63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 374 additions and 60 deletions

View File

@ -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"])

View File

@ -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.")

View File

@ -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. -->

40
tests/yaml/backtrack.yml Normal file
View File

@ -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

View File

@ -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

View File

@ -39,4 +39,4 @@ cases:
- D 1.0.0
- E 1.0.0
- F 1.0.0
skip: true
skip: old

92
tests/yaml/linter.py Normal file
View File

@ -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)

44
tests/yaml/overlap1.yml Normal file
View File

@ -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

37
tests/yaml/pip988.yml Normal file
View File

@ -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

24
tests/yaml/poetry2298.yml Normal file
View File

@ -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

View File

@ -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

24
tests/yaml/trivial.yml Normal file
View File

@ -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