diff options
Diffstat (limited to 'tests/test_validate.py')
-rw-r--r-- | tests/test_validate.py | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 index 0000000..21b918c --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,243 @@ +import errno +import pytest +import responses + +from flit import validate as fv + +def test_validate_entrypoints(): + assert fv.validate_entrypoints( + {'console_scripts': {'flit': 'flit:main'}}) == [] + assert fv.validate_entrypoints( + {'some.group': {'flit': 'flit.buildapi'}}) == [] + + res = fv.validate_entrypoints({'some.group': {'flit': 'a:b:c'}}) + assert len(res) == 1 + +def test_validate_name(): + def check(name): + return fv.validate_name({'name': name}) + + assert check('foo.bar_baz') == [] + assert check('5minus6') == [] + + assert len(check('_foo')) == 1 # Must start with alphanumeric + assert len(check('foo.')) == 1 # Must end with alphanumeric + assert len(check('Bücher')) == 1 # ASCII only + +def test_validate_requires_python(): + assert fv.validate_requires_python({}) == [] # Not required + + def check(spec): + return fv.validate_requires_python({'requires_python': spec}) + + assert check('>=3') == [] + assert check('>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*') == [] + + assert len(check('3')) == 1 + assert len(check('@12')) == 1 + assert len(check('>=2.7; !=3.0.*')) == 1 # Comma separated, not semicolon + +def test_validate_requires_dist(): + assert fv.validate_requires_dist({}) == [] # Not required + + def check(spec): + return fv.validate_requires_dist({'requires_dist': [spec]}) + + assert check('requests') == [] + assert check('requests[extra-foo]') == [] + assert check('requests (>=2.14)') == [] # parentheses allowed but not recommended + assert check('requests >=2.14') == [] + assert check('pexpect; sys_platform == "win32"') == [] + # Altogether now + assert check('requests[extra-foo] >=2.14; python_version < "3.0"') == [] + + # URL specifier + assert check('requests @ https://example.com/requests.tar.gz') == [] + assert check( + 'requests @ https://example.com/requests.tar.gz ; python_version < "3.8"' + ) == [] + + # Problems + assert len(check('Bücher')) == 1 + assert len(check('requests 2.14')) == 1 + assert len(check('pexpect; sys.platform == "win32"')) == 1 # '.' -> '_' + assert len(check('requests >=2.14 @ https://example.com/requests.tar.gz')) == 1 + # Several problems in one requirement + assert len(check('pexpect[_foo] =3; sys.platform == "win32"')) == 3 + +def test_validate_environment_marker(): + vem = fv.validate_environment_marker + + assert vem('python_version >= "3" and os_name == \'posix\'') == [] + + res = vem('python_version >= "3') # Unclosed string + assert len(res) == 1 + assert res[0].startswith("Invalid string") + + res = vem('python_verson >= "3"') # Misspelled name + assert len(res) == 1 + assert res[0].startswith("Invalid variable") + + res = vem("os_name is 'posix'") # No 'is' comparisons + assert len(res) == 1 + assert res[0].startswith("Invalid expression") + + res = vem("'2' < python_version < '4'") # No chained comparisons + assert len(res) == 1 + assert res[0].startswith("Invalid expression") + + assert len(vem('os.name == "linux\'')) == 2 + +def test_validate_url(): + vurl = fv.validate_url + assert vurl("https://github.com/pypa/flit") == [] + + assert len(vurl("github.com/pypa/flit")) == 1 + assert len(vurl("https://")) == 1 + + +def test_validate_project_urls(): + vpu = fv.validate_project_urls + + def check(prurl): + return vpu({'project_urls': [prurl]}) + assert vpu({}) == [] # Not required + assert check('Documentation, https://flit.readthedocs.io/') == [] + + # Missing https:// + assert len(check('Documentation, flit.readthedocs.io')) == 1 + # Double comma + assert len(check('A, B, flit.readthedocs.io')) == 1 + # No name + assert len(check(', https://flit.readthedocs.io/')) == 1 + # Name longer than 32 chars + assert len(check('Supercalifragilisticexpialidocious, https://flit.readthedocs.io/')) == 1 + + +def test_read_classifiers_cached(monkeypatch, tmp_path): + + def mock_get_cache_dir(): + tmp_file = tmp_path / "classifiers.lst" + with tmp_file.open("w") as fh: + fh.write("A\nB\nC") + return tmp_path + + monkeypatch.setattr(fv, "get_cache_dir", mock_get_cache_dir) + + classifiers = fv._read_classifiers_cached() + + assert classifiers == {'A', 'B', 'C'} + + +@responses.activate +def test_download_and_cache_classifiers(monkeypatch, tmp_path): + responses.add( + responses.GET, + 'https://pypi.org/pypi?%3Aaction=list_classifiers', + body="A\nB\nC") + + def mock_get_cache_dir(): + return tmp_path + + monkeypatch.setattr(fv, "get_cache_dir", mock_get_cache_dir) + + classifiers = fv._download_and_cache_classifiers() + + assert classifiers == {"A", "B", "C"} + + +def test_validate_classifiers_private(monkeypatch): + """ + Test that `Private :: Do Not Upload` considered a valid classifier. + This is a special case because it is not listed in a trove classifier + but it is a way to make sure that a private package is not get uploaded + on PyPI by accident. + + Implementation on PyPI side: + https://github.com/pypa/warehouse/pull/5440 + Issue about officially documenting the trick: + https://github.com/pypa/packaging.python.org/issues/643 + """ + monkeypatch.setattr(fv, "_read_classifiers_cached", lambda: set()) + + actual = fv.validate_classifiers({'invalid'}) + assert actual == ["Unrecognised classifier: 'invalid'"] + + assert fv.validate_classifiers({'Private :: Do Not Upload'}) == [] + + +@responses.activate +@pytest.mark.parametrize("error", [PermissionError, OSError(errno.EROFS, "")]) +def test_download_and_cache_classifiers_with_unacessible_dir(monkeypatch, error): + responses.add( + responses.GET, + 'https://pypi.org/pypi?%3Aaction=list_classifiers', + body="A\nB\nC") + + class MockCacheDir: + def mkdir(self, parents): + raise error + def __truediv__(self, other): + raise error + + monkeypatch.setattr(fv, "get_cache_dir", MockCacheDir) + + classifiers = fv._download_and_cache_classifiers() + + assert classifiers == {"A", "B", "C"} + + +def test_verify_classifiers_valid_classifiers(): + classifiers = {"A"} + valid_classifiers = {"A", "B"} + + problems = fv._verify_classifiers(classifiers, valid_classifiers) + + assert problems == [] + +def test_verify_classifiers_invalid_classifiers(): + classifiers = {"A", "B"} + valid_classifiers = {"A"} + + problems = fv._verify_classifiers(classifiers, valid_classifiers) + + assert problems == ["Unrecognised classifier: 'B'"] + +def test_validate_readme_rst(): + metadata = { + 'description_content_type': 'text/x-rst', + 'description': "Invalid ``rst'", + } + problems = fv.validate_readme_rst(metadata) + + assert len(problems) == 2 # 1 message that rst is invalid + 1 with details + assert "valid rst" in problems[0] + + # Markdown should be ignored + metadata = { + 'description_content_type': 'text/markdown', + 'description': "Invalid `rst'", + } + problems = fv.validate_readme_rst(metadata) + + assert problems == [] + +RST_WITH_CODE = """ +Code snippet: + +.. code-block:: python + + a = [i ** 2 for i in range(5)] +""" + +def test_validate_readme_rst_code(): + # Syntax highlighting shouldn't require pygments + metadata = { + 'description_content_type': 'text/x-rst', + 'description': RST_WITH_CODE, + } + problems = fv.validate_readme_rst(metadata) + for p in problems: + print(p) + + assert problems == [] |