diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:21:11 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:21:11 +0000 |
commit | cdb4a4e19b096cdbf1356e28287238122fc3599c (patch) | |
tree | c5ed3b2b40e4725bbaaae0710d1cbec21b23f3b0 /tests | |
parent | Initial commit. (diff) | |
download | python-installer-cdb4a4e19b096cdbf1356e28287238122fc3599c.tar.xz python-installer-cdb4a4e19b096cdbf1356e28287238122fc3599c.zip |
Adding upstream version 0.6.0+dfsg1.upstream/0.6.0+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
-rw-r--r-- | tests/conftest.py | 75 | ||||
-rw-r--r-- | tests/requirements.txt | 3 | ||||
-rw-r--r-- | tests/test_core.py | 905 | ||||
-rw-r--r-- | tests/test_destinations.py | 173 | ||||
-rw-r--r-- | tests/test_main.py | 69 | ||||
-rw-r--r-- | tests/test_records.py | 275 | ||||
-rw-r--r-- | tests/test_scripts.py | 70 | ||||
-rw-r--r-- | tests/test_sources.py | 94 | ||||
-rw-r--r-- | tests/test_utils.py | 232 |
9 files changed, 1896 insertions, 0 deletions
diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..029cb8f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,75 @@ +import textwrap +import zipfile + +import pytest + + +@pytest.fixture +def fancy_wheel(tmp_path): + path = tmp_path / "fancy-1.0.0-py2.py3-none-any.whl" + files = { + "fancy/": b"""""", + "fancy/__init__.py": b"""\ + def main(): + print("I'm fancy.") + """, + "fancy/__main__.py": b"""\ + if __name__ == "__main__": + from . import main + main() + """, + "fancy-1.0.0.data/data/fancy/": b"""""", + "fancy-1.0.0.data/data/fancy/data.py": b"""\ + # put me in data + """, + "fancy-1.0.0.dist-info/": b"""""", + "fancy-1.0.0.dist-info/top_level.txt": b"""\ + fancy + """, + "fancy-1.0.0.dist-info/entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "fancy-1.0.0.dist-info/WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "fancy-1.0.0.dist-info/METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + # The RECORD file is indirectly validated by the WheelFile, since it only + # provides the items that are a part of the wheel. + "fancy-1.0.0.dist-info/RECORD": b"""\ + fancy/__init__.py,, + fancy/__main__.py,, + fancy-1.0.0.data/data/fancy/data.py,, + fancy-1.0.0.dist-info/top_level.txt,, + fancy-1.0.0.dist-info/entry_points.txt,, + fancy-1.0.0.dist-info/WHEEL,, + fancy-1.0.0.dist-info/METADATA,, + fancy-1.0.0.dist-info/RECORD,, + """, + } + + with zipfile.ZipFile(path, "w") as archive: + for name, indented_content in files.items(): + archive.writestr( + name, + textwrap.dedent(indented_content.decode("utf-8")).encode("utf-8"), + ) + + return path diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..78b1a59 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,3 @@ +pytest +pytest-cov +pytest-xdist diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..1f3a44e --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,905 @@ +import hashlib +import textwrap +from io import BytesIO +from unittest import mock + +import pytest + +from installer import install +from installer.exceptions import InvalidWheelSource +from installer.records import RecordEntry +from installer.sources import WheelSource + + +# -------------------------------------------------------------------------------------- +# Helpers +# -------------------------------------------------------------------------------------- +def hash_and_size(data): + return hashlib.sha256(data).hexdigest(), len(data) + + +@pytest.fixture +def mock_destination(): + retval = mock.Mock() + + # A hacky approach to making sure we got the right objects going in. + def custom_write_file(scheme, path, stream, is_executable): + assert isinstance(stream, BytesIO) + return (path, scheme, 0) + + def custom_write_script(name, module, attr, section): + return (name, module, attr, section) + + retval.write_file.side_effect = custom_write_file + retval.write_script.side_effect = custom_write_script + + return retval + + +class FakeWheelSource(WheelSource): + def __init__(self, *, distribution, version, regular_files, dist_info_files): + super().__init__(distribution, version) + + self.dist_info_files = { + file: textwrap.dedent(content.decode("utf-8")) + for file, content in dist_info_files.items() + } + self.regular_files = { + file: textwrap.dedent(content.decode("utf-8")).encode("utf-8") + for file, content in regular_files.items() + } + + # Compute RECORD file. + _records = [record for record, _, _ in self.get_contents()] + self.dist_info_files["RECORD"] = "\n".join( + sorted( + ",".join([file, "sha256=" + hash_, str(size)]) + for file, hash_, size in _records + ) + ) + + @property + def dist_info_filenames(self): + return list(self.dist_info_files) + + def read_dist_info(self, filename): + return self.dist_info_files[filename] + + def get_contents(self): + # Sort for deterministic behaviour for Python versions that do not preserve + # insertion order for dictionaries. + for file, content in sorted(self.regular_files.items()): + hashed, size = hash_and_size(content) + record = (file, f"sha256={hashed}", str(size)) + with BytesIO(content) as stream: + yield record, stream, False + + # Sort for deterministic behaviour for Python versions that do not preserve + # insertion order for dictionaries. + for file, text in sorted(self.dist_info_files.items()): + content = text.encode("utf-8") + hashed, size = hash_and_size(content) + record = ( + self.dist_info_dir + "/" + file, + f"sha256={hashed}", + str(size), + ) + with BytesIO(content) as stream: + yield record, stream, False + + +# -------------------------------------------------------------------------------------- +# Actual Tests +# -------------------------------------------------------------------------------------- +class TestInstall: + def test_calls_destination_correctly(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + def main(): + print("I'm a fancy package") + """, + "fancy/__main__.py": b"""\ + if __name__ == "__main__": + from . import main + main() + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + install( + source=source, + destination=mock_destination, + additional_metadata={ + "fun_file.txt": b"this should be in dist-info!", + }, + ) + + mock_destination.assert_has_calls( + [ + mock.call.write_script( + name="fancy", + module="fancy", + attr="main", + section="console", + ), + mock.call.write_script( + name="fancy-gui", + module="fancy", + attr="main", + section="gui", + ), + mock.call.write_file( + scheme="purelib", + path="fancy/__init__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy/__main__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/METADATA", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/WHEEL", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/entry_points.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/top_level.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/fun_file.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.finalize_installation( + scheme="purelib", + record_file_path="fancy-1.0.0.dist-info/RECORD", + records=[ + ("scripts", ("fancy", "fancy", "main", "console")), + ("scripts", ("fancy-gui", "fancy", "main", "gui")), + ("purelib", ("fancy/__init__.py", "purelib", 0)), + ("purelib", ("fancy/__main__.py", "purelib", 0)), + ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)), + ("purelib", ("fancy-1.0.0.dist-info/WHEEL", "purelib", 0)), + ( + "purelib", + ("fancy-1.0.0.dist-info/entry_points.txt", "purelib", 0), + ), + ( + "purelib", + ("fancy-1.0.0.dist-info/top_level.txt", "purelib", 0), + ), + ( + "purelib", + ("fancy-1.0.0.dist-info/fun_file.txt", "purelib", 0), + ), + ( + "purelib", + RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None), + ), + ], + ), + ] + ) + + def test_no_entrypoints_is_ok(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + def main(): + print("I'm a fancy package") + """, + "fancy/__main__.py": b"""\ + if __name__ == "__main__": + from . import main + main() + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + install( + source=source, + destination=mock_destination, + additional_metadata={ + "fun_file.txt": b"this should be in dist-info!", + }, + ) + + mock_destination.assert_has_calls( + [ + mock.call.write_file( + scheme="purelib", + path="fancy/__init__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy/__main__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/METADATA", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/WHEEL", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/top_level.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/fun_file.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.finalize_installation( + scheme="purelib", + record_file_path="fancy-1.0.0.dist-info/RECORD", + records=[ + ("purelib", ("fancy/__init__.py", "purelib", 0)), + ("purelib", ("fancy/__main__.py", "purelib", 0)), + ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)), + ("purelib", ("fancy-1.0.0.dist-info/WHEEL", "purelib", 0)), + ( + "purelib", + ("fancy-1.0.0.dist-info/top_level.txt", "purelib", 0), + ), + ( + "purelib", + ("fancy-1.0.0.dist-info/fun_file.txt", "purelib", 0), + ), + ( + "purelib", + RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None), + ), + ], + ), + ] + ) + + def test_handles_platlib(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + def main(): + print("I'm a fancy package") + """, + "fancy/__main__.py": b"""\ + if __name__ == "__main__": + from . import main + main() + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: false + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + install( + source=source, + destination=mock_destination, + additional_metadata={ + "fun_file.txt": b"this should be in dist-info!", + }, + ) + + mock_destination.assert_has_calls( + [ + mock.call.write_script( + name="fancy", + module="fancy", + attr="main", + section="console", + ), + mock.call.write_script( + name="fancy-gui", + module="fancy", + attr="main", + section="gui", + ), + mock.call.write_file( + scheme="platlib", + path="fancy/__init__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy/__main__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy-1.0.0.dist-info/METADATA", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy-1.0.0.dist-info/WHEEL", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy-1.0.0.dist-info/entry_points.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy-1.0.0.dist-info/top_level.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy-1.0.0.dist-info/fun_file.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.finalize_installation( + scheme="platlib", + record_file_path="fancy-1.0.0.dist-info/RECORD", + records=[ + ("scripts", ("fancy", "fancy", "main", "console")), + ("scripts", ("fancy-gui", "fancy", "main", "gui")), + ("platlib", ("fancy/__init__.py", "platlib", 0)), + ("platlib", ("fancy/__main__.py", "platlib", 0)), + ("platlib", ("fancy-1.0.0.dist-info/METADATA", "platlib", 0)), + ("platlib", ("fancy-1.0.0.dist-info/WHEEL", "platlib", 0)), + ( + "platlib", + ("fancy-1.0.0.dist-info/entry_points.txt", "platlib", 0), + ), + ( + "platlib", + ("fancy-1.0.0.dist-info/top_level.txt", "platlib", 0), + ), + ( + "platlib", + ("fancy-1.0.0.dist-info/fun_file.txt", "platlib", 0), + ), + ( + "platlib", + RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None), + ), + ], + ), + ] + ) + + def test_accepts_newer_minor_wheel_versions(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + def main(): + print("I'm a fancy package") + """, + "fancy/__main__.py": b"""\ + if __name__ == "__main__": + from . import main + main() + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "WHEEL": b"""\ + Wheel-Version: 1.1 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + install( + source=source, + destination=mock_destination, + additional_metadata={ + "fun_file.txt": b"this should be in dist-info!", + }, + ) + + # no assertions necessary, since we want to make sure this test didn't + # raises errors. + assert True + + def test_rejects_newer_major_wheel_versions(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + def main(): + print("I'm a fancy package") + """, + "fancy/__main__.py": b"""\ + if __name__ == "__main__": + from . import main + main() + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "WHEEL": b"""\ + Wheel-Version: 2.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + with pytest.raises(InvalidWheelSource) as ctx: + install( + source=source, + destination=mock_destination, + additional_metadata={ + "fun_file.txt": b"this should be in dist-info!", + }, + ) + + assert "Incompatible Wheel-Version" in str(ctx.value) + + def test_handles_data_properly(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + # put me in purelib + """, + "fancy-1.0.0.data/purelib/fancy/purelib.py": b"""\ + # put me in purelib + """, + "fancy-1.0.0.data/platlib/fancy/platlib.py": b"""\ + # put me in platlib + """, + "fancy-1.0.0.data/scripts/fancy/scripts.py": b"""\ + # put me in scripts + """, + "fancy-1.0.0.data/headers/fancy/headers.py": b"""\ + # put me in headers + """, + "fancy-1.0.0.data/data/fancy/data.py": b"""\ + # put me in data + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + install( + source=source, + destination=mock_destination, + additional_metadata={}, + ) + + mock_destination.assert_has_calls( + [ + mock.call.write_script( + name="fancy", + module="fancy", + attr="main", + section="console", + ), + mock.call.write_script( + name="fancy-gui", + module="fancy", + attr="main", + section="gui", + ), + mock.call.write_file( + scheme="data", + path="fancy/data.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="headers", + path="fancy/headers.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="platlib", + path="fancy/platlib.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy/purelib.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="scripts", + path="fancy/scripts.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy/__init__.py", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/METADATA", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/WHEEL", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/entry_points.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/top_level.txt", + stream=mock.ANY, + is_executable=False, + ), + mock.call.finalize_installation( + scheme="purelib", + record_file_path="fancy-1.0.0.dist-info/RECORD", + records=[ + ("scripts", ("fancy", "fancy", "main", "console")), + ("scripts", ("fancy-gui", "fancy", "main", "gui")), + ("data", ("fancy/data.py", "data", 0)), + ("headers", ("fancy/headers.py", "headers", 0)), + ("platlib", ("fancy/platlib.py", "platlib", 0)), + ("purelib", ("fancy/purelib.py", "purelib", 0)), + ("scripts", ("fancy/scripts.py", "scripts", 0)), + ("purelib", ("fancy/__init__.py", "purelib", 0)), + ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)), + ("purelib", ("fancy-1.0.0.dist-info/WHEEL", "purelib", 0)), + ( + "purelib", + ("fancy-1.0.0.dist-info/entry_points.txt", "purelib", 0), + ), + ( + "purelib", + ("fancy-1.0.0.dist-info/top_level.txt", "purelib", 0), + ), + ( + "purelib", + RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None), + ), + ], + ), + ] + ) + + def test_errors_out_when_given_invalid_scheme_in_data(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + # put me in purelib + """, + "fancy-1.0.0.data/purelib/fancy/purelib.py": b"""\ + # put me in purelib + """, + "fancy-1.0.0.data/invalid/fancy/invalid.py": b"""\ + # i am invalid + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "entry_points.txt": b"""\ + [console_scripts] + fancy = fancy:main + + [gui_scripts] + fancy-gui = fancy:main + """, + "WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + + with pytest.raises(InvalidWheelSource) as ctx: + install( + source=source, + destination=mock_destination, + additional_metadata={}, + ) + + assert "fancy-1.0.0.data/invalid/fancy/invalid.py" in str(ctx.value) + + def test_ensure_non_executable_for_additional_metadata(self, mock_destination): + # Create a fake wheel + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"""\ + # put me in purelib + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "WHEEL": b"""\ + Wheel-Version: 1.0 + Generator: magic (1.0.0) + Root-Is-Purelib: true + Tag: py3-none-any + """, + "METADATA": b"""\ + Metadata-Version: 2.1 + Name: fancy + Version: 1.0.0 + Summary: A fancy package + Author: Agendaless Consulting + Author-email: nobody@example.com + License: MIT + Keywords: fancy amazing + Platform: UNKNOWN + Classifier: Intended Audience :: Developers + """, + }, + ) + all_contents = list(source.get_contents()) + source.get_contents = lambda: ( + (*contents, True) for (*contents, _) in all_contents + ) + install( + source=source, + destination=mock_destination, + additional_metadata={ + "fun_file.txt": b"this should be in dist-info!", + }, + ) + + mock_destination.assert_has_calls( + [ + mock.call.write_file( + scheme="purelib", + path="fancy/__init__.py", + stream=mock.ANY, + is_executable=True, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/METADATA", + stream=mock.ANY, + is_executable=True, + ), + mock.call.write_file( + scheme="purelib", + path="fancy-1.0.0.dist-info/fun_file.txt", + stream=mock.ANY, + is_executable=False, + ), + ], + any_order=True, + ) diff --git a/tests/test_destinations.py b/tests/test_destinations.py new file mode 100644 index 0000000..225b009 --- /dev/null +++ b/tests/test_destinations.py @@ -0,0 +1,173 @@ +import io +import os.path + +import pytest + +from installer.destinations import SchemeDictionaryDestination, WheelDestination +from installer.records import RecordEntry +from installer.scripts import Script +from installer.utils import SCHEME_NAMES + + +class TestWheelDestination: + def test_takes_no_arguments(self): + WheelDestination() + + def test_raises_not_implemented_error(self): + destination = WheelDestination() + + with pytest.raises(NotImplementedError): + destination.write_script(name=None, module=None, attr=None, section=None) + + with pytest.raises(NotImplementedError): + destination.write_file( + scheme=None, path=None, stream=None, is_executable=False + ) + + with pytest.raises(NotImplementedError): + destination.finalize_installation( + scheme=None, + record_file_path=None, + records=None, + ) + + +class TestSchemeDictionaryDestination: + @pytest.fixture() + def destination(self, tmp_path): + scheme_dict = {} + for scheme in SCHEME_NAMES: + full_path = tmp_path / scheme + if not full_path.exists(): + full_path.mkdir() + scheme_dict[scheme] = str(full_path) + return SchemeDictionaryDestination(scheme_dict, "/my/python", "posix") + + @pytest.mark.parametrize( + ("scheme", "path", "data", "expected"), + [ + pytest.param( + "data", "my_data.bin", b"my data", b"my data", id="normal file" + ), + pytest.param( + "data", + "data_folder/my_data.bin", + b"my data", + b"my data", + id="normal file in subfolder", + ), + pytest.param( + "scripts", + "my_script.py", + b"#!python\nmy script", + b"#!/my/python\nmy script", + id="script file", + ), + pytest.param( + "scripts", + "script_folder/my_script.py", + b"#!python\nmy script", + b"#!/my/python\nmy script", + id="script file in subfolder", + ), + ], + ) + def test_write_file(self, destination, scheme, path, data, expected): + record = destination.write_file(scheme, path, io.BytesIO(data), False) + file_path = os.path.join(destination.scheme_dict[scheme], path) + with open(file_path, "rb") as f: + file_data = f.read() + + assert file_data == expected + assert record.path == path + + def test_write_record_duplicate(self, destination): + destination.write_file("data", "my_data.bin", io.BytesIO(b"my data"), False) + with pytest.raises(FileExistsError): + destination.write_file("data", "my_data.bin", io.BytesIO(b"my data"), False) + + def test_write_script(self, destination): + script_args = ("my_entrypoint", "my_module", "my_function", "console") + record = destination.write_script(*script_args) + file_path = os.path.join(destination.scheme_dict["scripts"], "my_entrypoint") + + assert os.path.isfile(file_path) + + with open(file_path, "rb") as f: + file_data = f.read() + name, expected_data = Script(*script_args).generate("/my/python", "posix") + + assert file_data == expected_data + assert record.path == "my_entrypoint" + + def test_finalize_write_record(self, destination): + records = [ + ( + "data", + destination.write_file( + "data", + "my_data1.bin", + io.BytesIO(b"my data 1"), + is_executable=False, + ), + ), + ( + "data", + destination.write_file( + "data", + "my_data2.bin", + io.BytesIO(b"my data 2"), + is_executable=False, + ), + ), + ( + "data", + destination.write_file( + "data", + "my_data3,my_data4.bin", + io.BytesIO(b"my data 3"), + is_executable=False, + ), + ), + ( + "scripts", + destination.write_file( + "scripts", + "my_script", + io.BytesIO(b"my script"), + is_executable=True, + ), + ), + ( + "scripts", + destination.write_file( + "scripts", + "my_script2", + io.BytesIO(b"#!python\nmy script"), + is_executable=False, + ), + ), + ( + "scripts", + destination.write_script( + "my_entrypoint", "my_module", "my_function", "console" + ), + ), + ("purelib", RecordEntry("RECORD", None, None)), + ] + + destination.finalize_installation("purelib", "RECORD", records) + file_path = os.path.join(destination.scheme_dict["purelib"], "RECORD") + + with open(file_path, "rb") as f: + data = f.read() + + assert data == ( + b"../data/my_data1.bin,sha256=NV0A-M4OPuqTsHjeD6Wth_-UqrpAAAdyplcustFZ8s4,9\n" + b"../data/my_data2.bin,sha256=lP7V8oWLqgyXCbdASNiPdsUogzPUZhht_7F8T5bC3eQ,9\n" + b'"../data/my_data3,my_data4.bin",sha256=18krruu1gr01x-WM_9ChSASoHv0mfRAV6-B2bd9sxpo,9\n' + b"../scripts/my_script,sha256=M60fWvUSMJkPtw2apUvjWWwOcnRPcVy_zO4-4lpH08o,9\n" + b"../scripts/my_script2,sha256=k9_997kTbTYQm7EXFLclVZL1m2N98rU90QX46XeMvjY,22\n" + b"../scripts/my_entrypoint,sha256=_p_9nwmeIeoMBfQ0akhr1KbKn3laDydg0J7cy0Fs6JI,216\n" + b"RECORD,,\n" + ) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..391a13d --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,69 @@ +import os + +from installer.__main__ import _get_scheme_dict as get_scheme_dict +from installer.__main__ import _main as main + + +def test_get_scheme_dict(): + d = get_scheme_dict(distribution_name="foo") + assert set(d.keys()) >= {"purelib", "platlib", "headers", "scripts", "data"} + + +def test_get_scheme_dict_prefix(): + d = get_scheme_dict(distribution_name="foo", prefix="/foo") + for key in ("purelib", "platlib", "headers", "scripts", "data"): + assert d[key].startswith( + f"{os.sep}foo" + ), f"{key} does not start with /foo: {d[key]}" + + +def test_main(fancy_wheel, tmp_path): + destdir = tmp_path / "dest" + + main([str(fancy_wheel), "-d", str(destdir)], "python -m installer") + + installed_py_files = destdir.rglob("*.py") + + assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"} + + installed_pyc_files = destdir.rglob("*.pyc") + assert {f.name.split(".")[0] for f in installed_pyc_files} == { + "__init__", + "__main__", + } + + +def test_main_prefix(fancy_wheel, tmp_path): + destdir = tmp_path / "dest" + + main([str(fancy_wheel), "-d", str(destdir), "-p", "/foo"], "python -m installer") + + installed_py_files = list(destdir.rglob("*.py")) + + for f in installed_py_files: + assert str(f.parent).startswith( + str(destdir / "foo") + ), f"path does not respect destdir+prefix: {f}" + assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"} + + installed_pyc_files = destdir.rglob("*.pyc") + assert {f.name.split(".")[0] for f in installed_pyc_files} == { + "__init__", + "__main__", + } + + +def test_main_no_pyc(fancy_wheel, tmp_path): + destdir = tmp_path / "dest" + + main( + [str(fancy_wheel), "-d", str(destdir), "--no-compile-bytecode"], + "python -m installer", + ) + + installed_py_files = destdir.rglob("*.py") + + assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"} + + installed_pyc_files = destdir.rglob("*.pyc") + assert set(installed_pyc_files) == set() diff --git a/tests/test_records.py b/tests/test_records.py new file mode 100644 index 0000000..45427ae --- /dev/null +++ b/tests/test_records.py @@ -0,0 +1,275 @@ +import pytest + +from installer.records import Hash, InvalidRecordEntry, RecordEntry, parse_record_file + + +# +# pytest fixture witchcraft +# +@pytest.fixture() +def record_simple_list(): + return [ + "file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144", + "distribution-1.0.dist-info/RECORD,,", + ] + + +@pytest.fixture() +def record_simple_iter(record_simple_list): + return iter(record_simple_list) + + +@pytest.fixture() +def record_simple_file(tmpdir, record_simple_list): + p = tmpdir.join("RECORD") + p.write("\n".join(record_simple_list)) + with open(str(p)) as f: + yield f + + +@pytest.fixture() +def record_input(request): + return request.getfixturevalue(request.param) + + +SAMPLE_RECORDS = [ + ( + "purelib", + ("test1.py", "sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4", 6), + b"test1\n", + True, + ), + ( + "purelib", + ("test2.py", "sha256=fW_Xd08Nh2JNptzxbQ09EEwxkedx--LznIau1LK_Gg8", 6), + b"test2\n", + True, + ), + ( + "purelib", + ("test3.py", "sha256=qwPDTx7OCCEf4qgDn9ZCQZmz9de1X_E7ETSzZHdsRcU", 6), + b"test3\n", + True, + ), + ( + "purelib", + ("test4.py", "sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4", 7), + b"test1\n", + False, + ), + ( + "purelib", + ( + "test5.py", + "sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4", + None, + ), + b"test1\n", + True, + ), + ("purelib", ("test6.py", None, None), b"test1\n", True), +] + + +# +# Actual Tests +# +class TestRecordEntry: + @pytest.mark.parametrize( + "path, hash_, size, caused_by", + [ + ("", "", "", ["path"]), + ("", "", "non-int", ["path", "size"]), + ("a.py", "", "non-int", ["size"]), + # Notice that we're explicitly allowing non-compliant hash values + ("a.py", "some-random-value", "non-int", ["size"]), + ], + ) + def test_invalid_elements(self, path, hash_, size, caused_by): + with pytest.raises(InvalidRecordEntry) as exc_info: + RecordEntry.from_elements(path, hash_, size) + + assert exc_info.value.elements == (path, hash_, size) + for word in caused_by: + assert word in str(exc_info.value) + + @pytest.mark.parametrize( + "path, hash_, size", + [ + ("a.py", "", ""), + ("a.py", "", "3144"), + ("a.py", "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", ""), + ("a.py", "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", "3144"), + ], + ) + def test_valid_elements(self, path, hash_, size): + RecordEntry.from_elements(path, hash_, size) + + @pytest.mark.parametrize( + ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS + ) + def test_populates_attributes_correctly( + self, scheme, elements, data, passes_validation + ): + path, hash_string, size = elements + + record = RecordEntry.from_elements(path, hash_string, size) + + assert record.path == path + assert record.size == size + + if record.hash_ is not None: + assert isinstance(record.hash_, Hash) + assert record.hash_.name == "sha256" + assert record.hash_.value == hash_string[len("sha256=") :] + + @pytest.mark.parametrize( + ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS + ) + def test_validation(self, scheme, elements, data, passes_validation): + record = RecordEntry.from_elements(*elements) + assert record.validate(data) == passes_validation + + @pytest.mark.parametrize( + ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS + ) + def test_string_representation(self, scheme, elements, data, passes_validation): + record = RecordEntry.from_elements(*elements) + + expected_row = tuple( + [(str(elem) if elem is not None else "") for elem in elements] + ) + assert record.to_row() == expected_row + + @pytest.mark.parametrize( + ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS + ) + def test_string_representation_with_prefix( + self, scheme, elements, data, passes_validation + ): + record = RecordEntry.from_elements(*elements) + + expected_row = tuple( + [ + (str(elem) if elem is not None else "") + for elem in ("prefix/" + elements[0], elements[1], elements[2]) + ] + ) + assert record.to_row("prefix/") == expected_row + + def test_equality(self): + record = RecordEntry.from_elements( + "file.py", + "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "3144", + ) + record_same = RecordEntry.from_elements( + "file.py", + "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "3144", + ) + record_different_name = RecordEntry.from_elements( + "file2.py", + "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "3144", + ) + record_different_hash_name = RecordEntry.from_elements( + "file.py", + "md5=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "3144", + ) + record_different_hash_value = RecordEntry.from_elements( + "file.py", + "sha256=qwertyuiodfdsflkgshdlkjghrefawrwerwffsdfflk29", + "3144", + ) + record_different_size = RecordEntry.from_elements( + "file.py", + "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "10", + ) + + assert record == record_same + + assert record != "random string" + assert record != record_different_name + assert record != record_different_hash_name + assert record != record_different_hash_value + assert record != record_different_size + + # Ensure equality is based on current state + record_same.hash_ = None + assert record != record_same + + +class TestParseRecordFile: + def test_accepts_empty_iterable(self): + list(parse_record_file([])) + + @pytest.mark.parametrize( + "record_input", + ["record_simple_list", "record_simple_iter", "record_simple_file"], + indirect=True, + ) + def test_accepts_all_kinds_of_iterables(self, record_input): + """Should accepts any iterable, e.g. container, iterator, or file object.""" + records = list(parse_record_file(record_input)) + assert len(records) == 2 + + assert records == [ + ( + "file.py", + "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "3144", + ), + ("distribution-1.0.dist-info/RECORD", "", ""), + ] + + @pytest.mark.parametrize( + "line, element_count", + [ + pytest.param( + "file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144,", + 4, + id="four", + ), + pytest.param( + "distribution-1.0.dist-info/RECORD,,,,", + 5, + id="five", + ), + ], + ) + def test_rejects_wrong_element_count(self, line, element_count): + with pytest.raises(InvalidRecordEntry) as exc_info: + list(parse_record_file([line])) + + message = f"expected 3 elements, got {element_count}" + assert message in str(exc_info.value) + + def test_shows_correct_row_number(self): + record_lines = [ + "file1.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144", + "file2.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144", + "file3.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144", + "distribution-1.0.dist-info/RECORD,,,,", + ] + with pytest.raises(InvalidRecordEntry) as exc_info: + list(parse_record_file(record_lines)) + + assert "Row Index 3" in str(exc_info.value) + + def test_parse_record_entry_with_comma(self): + record_lines = [ + '"file1,file2.txt",sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144', + "distribution-1.0.dist-info/RECORD,,", + ] + records = list(parse_record_file(record_lines)) + assert records == [ + ( + "file1,file2.txt", + "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", + "3144", + ), + ("distribution-1.0.dist-info/RECORD", "", ""), + ] diff --git a/tests/test_scripts.py b/tests/test_scripts.py new file mode 100644 index 0000000..2da6577 --- /dev/null +++ b/tests/test_scripts.py @@ -0,0 +1,70 @@ +import io +import os +import zipfile + +import pytest + +from installer import _scripts +from installer.scripts import InvalidScript, Script + + +def test_script_generate_simple(): + script = Script("foo", "foo.bar", "baz.qux", section="console") + name, data = script.generate("/path/to/my/python", kind="posix") + + assert name == "foo" + assert data.startswith(b"#!/path/to/my/python\n") + assert b"\nfrom foo.bar import baz\n" in data + assert b"baz.qux()" in data + + +def test_script_generate_space_in_executable(): + script = Script("foo", "foo.bar", "baz.qux", section="console") + name, data = script.generate("/path to my/python", kind="posix") + + assert name == "foo" + assert data.startswith(b"#!/bin/sh\n") + assert b" '/path to my/python'" in data + assert b"\nfrom foo.bar import baz\n" in data + assert b"baz.qux()" in data + + +def _read_launcher_data(section, kind): + prefix = {"console": "t", "gui": "w"}[section] + suffix = {"win-ia32": "32", "win-amd64": "64", "win-arm": "_arm"}[kind] + filename = os.path.join( + os.path.dirname(os.path.abspath(_scripts.__file__)), + f"{prefix}{suffix}.exe", + ) + with open(filename, "rb") as f: + return f.read() + + +@pytest.mark.parametrize("section", ["console", "gui"]) +@pytest.mark.parametrize("kind", ["win-ia32", "win-amd64", "win-arm"]) +def test_script_generate_launcher(section, kind): + launcher_data = _read_launcher_data(section, kind) + + script = Script("foo", "foo.bar", "baz.qux", section=section) + name, data = script.generate("#!C:\\path to my\\python.exe\n", kind=kind) + + prefix_len = len(launcher_data) + len(b"#!C:\\path to my\\python.exe\n") + stream = io.BytesIO(data[prefix_len:]) + with zipfile.ZipFile(stream) as zf: + code = zf.read("__main__.py") + + assert name == "foo.exe" + assert data.startswith(launcher_data) + assert b"#!C:\\path to my\\python.exe\n" in data + assert b"\nfrom foo.bar import baz\n" in code + assert b"baz.qux()" in code + + +@pytest.mark.parametrize( + "section, kind", + [("nonexist", "win-ia32"), ("console", "nonexist"), ("nonexist", "nonexist")], +) +def test_script_generate_launcher_error(section, kind): + script = Script("foo", "foo.bar", "baz.qux", section=section) + with pytest.raises(InvalidScript): + script.generate("#!C:\\path to my\\python.exe\n", kind=kind) diff --git a/tests/test_sources.py b/tests/test_sources.py new file mode 100644 index 0000000..a79cc24 --- /dev/null +++ b/tests/test_sources.py @@ -0,0 +1,94 @@ +import posixpath +import zipfile + +import pytest + +from installer.records import parse_record_file +from installer.sources import WheelFile, WheelSource + + +class TestWheelSource: + def test_takes_two_arguments(self): + WheelSource("distribution", "version") + WheelSource(distribution="distribution", version="version") + + def test_correctly_computes_properties(self): + source = WheelSource(distribution="distribution", version="version") + + assert source.data_dir == "distribution-version.data" + assert source.dist_info_dir == "distribution-version.dist-info" + + def test_raises_not_implemented_error(self): + source = WheelSource(distribution="distribution", version="version") + + with pytest.raises(NotImplementedError): + source.dist_info_filenames + + with pytest.raises(NotImplementedError): + source.read_dist_info("METADATA") + + with pytest.raises(NotImplementedError): + source.get_contents() + + +class TestWheelFile: + def test_rejects_not_okay_name(self, tmp_path): + # Create an empty zipfile + path = tmp_path / "not_a_valid_name.whl" + with zipfile.ZipFile(str(path), "w"): + pass + + with pytest.raises(ValueError, match="Not a valid wheel filename: .+"): + with WheelFile.open(str(path)): + pass + + def test_provides_correct_dist_info_filenames(self, fancy_wheel): + with WheelFile.open(fancy_wheel) as source: + assert sorted(source.dist_info_filenames) == [ + "METADATA", + "RECORD", + "WHEEL", + "entry_points.txt", + "top_level.txt", + ] + + def test_correctly_reads_from_dist_info_files(self, fancy_wheel): + files = {} + with zipfile.ZipFile(fancy_wheel) as archive: + for file in archive.namelist(): + if ".dist-info" not in file: + continue + files[posixpath.basename(file)] = archive.read(file).decode("utf-8") + + got_files = {} + with WheelFile.open(fancy_wheel) as source: + for file in files: + got_files[file] = source.read_dist_info(file) + + assert got_files == files + + def test_provides_correct_contents(self, fancy_wheel): + # Know the contents of the wheel + files = {} + with zipfile.ZipFile(fancy_wheel) as archive: + for file in archive.namelist(): + if file[-1:] == "/": + continue + files[file] = archive.read(file) + + expected_record_lines = ( + files["fancy-1.0.0.dist-info/RECORD"].decode("utf-8").splitlines() + ) + expected_records = list(parse_record_file(expected_record_lines)) + + # Check that the object's output is appropriate + got_records = [] + got_files = {} + with WheelFile.open(fancy_wheel) as source: + for record_elements, stream, is_executable in source.get_contents(): + got_records.append(record_elements) + got_files[record_elements[0]] = stream.read() + assert not is_executable + + assert sorted(got_records) == sorted(expected_records) + assert got_files == files diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..bfcc089 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,232 @@ +"""Tests for installer.utils +""" + +import base64 +import hashlib +import textwrap +from email.message import Message +from io import BytesIO + +import pytest +from test_records import SAMPLE_RECORDS + +from installer.records import RecordEntry +from installer.utils import ( + WheelFilename, + construct_record_file, + copyfileobj_with_hashing, + fix_shebang, + parse_entrypoints, + parse_metadata_file, + parse_wheel_filename, +) + + +class TestParseMetadata: + def test_basics(self): + result = parse_metadata_file( + textwrap.dedent( + """\ + Name: package + Version: 1.0.0 + Multi-Use-Field: 1 + Multi-Use-Field: 2 + Multi-Use-Field: 3 + """ + ) + ) + assert isinstance(result, Message) + assert result.get("Name") == "package" + assert result.get("version") == "1.0.0" + assert result.get_all("MULTI-USE-FIELD") == ["1", "2", "3"] + + +class TestParseWheelFilename: + @pytest.mark.parametrize( + "string, expected", + [ + # Crafted package name w/ a "complex" version and build tag + ( + "package-1!1.0+abc.7-753-py3-none-any.whl", + WheelFilename("package", "1!1.0+abc.7", "753", "py3-none-any"), + ), + # Crafted package name w/ a "complex" version and no build tag + ( + "package-1!1.0+abc.7-py3-none-any.whl", + WheelFilename("package", "1!1.0+abc.7", None, "py3-none-any"), + ), + # Use real tensorflow wheel names + ( + "tensorflow-2.3.0-cp38-cp38-macosx_10_11_x86_64.whl", + WheelFilename( + "tensorflow", "2.3.0", None, "cp38-cp38-macosx_10_11_x86_64" + ), + ), + ( + "tensorflow-2.3.0-cp38-cp38-manylinux2010_x86_64.whl", + WheelFilename( + "tensorflow", "2.3.0", None, "cp38-cp38-manylinux2010_x86_64" + ), + ), + ( + "tensorflow-2.3.0-cp38-cp38-win_amd64.whl", + WheelFilename("tensorflow", "2.3.0", None, "cp38-cp38-win_amd64"), + ), + ], + ) + def test_valid_cases(self, string, expected): + got = parse_wheel_filename(string) + assert expected == got, (expected, got) + + @pytest.mark.parametrize( + "string", + [ + # Not ".whl" + "pip-20.0.0-py2.py3-none-any.zip", + # No tag + "pip-20.0.0.whl", + # Empty tag + "pip-20.0.0---.whl", + ], + ) + def test_invalid_cases(self, string): + with pytest.raises(ValueError): + parse_wheel_filename(string) + + +class TestCopyFileObjWithHashing: + def test_basic_functionality(self): + data = b"input data is this" + hash_ = ( + base64.urlsafe_b64encode(hashlib.sha256(data).digest()) + .decode("ascii") + .rstrip("=") + ) + size = len(data) + + with BytesIO(data) as source: + with BytesIO() as dest: + result = copyfileobj_with_hashing(source, dest, hash_algorithm="sha256") + written_data = dest.getvalue() + + assert result == (hash_, size) + assert written_data == data + + +class TestScript: + @pytest.mark.parametrize( + ("data", "expected"), + [ + pytest.param( + b"#!python\ntest", + b"#!/my/python\ntest", + id="python", + ), + pytest.param( + b"#!pythonw\ntest", + b"#!/my/python\ntest", + id="pythonw", + ), + pytest.param( + b"#!python something\ntest", + b"#!/my/python\ntest", + id="python-with-args", + ), + pytest.param( + b"#!python", + b"#!/my/python\n", + id="python-no-content", + ), + ], + ) + def test_replace_shebang(self, data, expected): + with BytesIO(data) as source: + with fix_shebang(source, "/my/python") as stream: + result = stream.read() + assert result == expected + + @pytest.mark.parametrize( + "data", + [ + b"#!py\ntest", + b"#!something\ntest", + b"#something\ntest", + b"#something", + b"something", + ], + ) + def test_keep_data(self, data): + with BytesIO(data) as source: + with fix_shebang(source, "/my/python") as stream: + result = stream.read() + assert result == data + + +class TestConstructRecord: + def test_construct(self): + records = [ + (scheme, RecordEntry.from_elements(*elements)) + for scheme, elements, _, _ in SAMPLE_RECORDS + ] + assert construct_record_file(records).read() == ( + b"test1.py,sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4,6\n" + b"test2.py,sha256=fW_Xd08Nh2JNptzxbQ09EEwxkedx--LznIau1LK_Gg8,6\n" + b"test3.py,sha256=qwPDTx7OCCEf4qgDn9ZCQZmz9de1X_E7ETSzZHdsRcU,6\n" + b"test4.py,sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4,7\n" + b"test5.py,sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4,\n" + b"test6.py,,\n" + ) + + +class TestParseEntryPoints: + @pytest.mark.parametrize( + ("script", "expected"), + [ + pytest.param("", [], id="empty"), + pytest.param( + """ + [foo] + foo = foo.bar + """, + [], + id="unrelated", + ), + pytest.param( + """ + [console_scripts] + package = package.__main__:package + """, + [ + ("package", "package.__main__", "package", "console"), + ], + id="cli", + ), + pytest.param( + """ + [gui_scripts] + package = package.__main__:package + """, + [ + ("package", "package.__main__", "package", "gui"), + ], + id="gui", + ), + pytest.param( + """ + [console_scripts] + magic-cli = magic.cli:main + + [gui_scripts] + magic-gui = magic.gui:main + """, + [ + ("magic-cli", "magic.cli", "main", "console"), + ("magic-gui", "magic.gui", "main", "gui"), + ], + id="cli-and-gui", + ), + ], + ) + def test_valid(self, script, expected): + iterable = parse_entrypoints(textwrap.dedent(script)) + assert list(iterable) == expected, expected |