tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit edbbc5865a1b5fb5048cd59ecc1d570280db5ad5
parent 8fa480f29a0843345e6f117c1a2be6ff4d6b1795
Author: Andrew Halberstadt <ahal@mozilla.com>
Date:   Fri, 14 Nov 2025 14:23:33 +0000

Bug 1999798 - Add tests and docs for new 'requirements-txt' specifier, r=ahochheiden

Differential Revision: https://phabricator.services.mozilla.com/D272489

Diffstat:
Mpython/docs/index.rst | 12+++++++++++-
Mpython/mach/mach/test/test_site.py | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpython/mozbuild/mozbuild/test/python.toml | 2++
Apython/mozbuild/mozbuild/test/test_site_dependency_extractor.py | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 198 insertions(+), 1 deletion(-)

diff --git a/python/docs/index.rst b/python/docs/index.rst @@ -51,7 +51,17 @@ To add a ``pip install``-d package dependency, add it to your site's ... pypi:new-package==<version> - ... + +If you'd like to lock dependencies and validate hashes, you can alternatively specify a path +to a ``requirements.txt`` file: + +.. code:: text + + ... + requirements-txt:path/to/requirements.txt + +The ``requirements.txt`` file can be generated using any tool you like, but it must include +hashes for all listed packages. .. note:: diff --git a/python/mach/mach/test/test_site.py b/python/mach/mach/test/test_site.py @@ -3,15 +3,22 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os +import subprocess +import sys from unittest import mock import pytest from buildconfig import topsrcdir from mozunit import main +from mach.requirements import MachEnvRequirements, RequirementsTxtSpecifier from mach.site import ( PIP_NETWORK_INSTALL_RESTRICTED_VIRTUALENVS, + ExternalPythonSite, + MozSiteMetadata, + PythonVirtualenv, SitePackagesSource, + _create_venv_with_pthfile, resolve_requirements, ) @@ -52,5 +59,108 @@ def test_all_restricted_sites_dont_have_pypi_requirements(): ) +@pytest.fixture +def parse_requirements_txt(tmp_path): + """Fixture to test requirements-txt parsing.""" + + def inner(site_content, requirements_txt_content=None): + # Create site packages file + site_file = tmp_path / "test-site.txt" + site_file.write_text(site_content) + + # Create requirements.txt if provided + if requirements_txt_content is not None: + req_file = tmp_path / "requirements.txt" + req_file.write_text(requirements_txt_content) + + # Parse the site file + requirements = MachEnvRequirements.from_requirements_definition( + str(tmp_path), + is_thunderbird=False, + only_strict_requirements=False, + requirements_definition=str(site_file), + ) + return requirements + + return inner + + +def test_requirements_txt_parsed(parse_requirements_txt): + requirements = parse_requirements_txt( + "requires-python:>=3.9\nrequirements-txt:requirements.txt\n", + "package1==1.0.0 --hash=sha256:abc123\npackage2==2.0.0 --hash=sha256:def456\n", + ) + + assert len(requirements.requirements_txt_files) == 1 + assert "requirements.txt" in requirements.requirements_txt_files[0].path + + +def test_requirements_txt_missing_file(parse_requirements_txt): + with pytest.raises(Exception, match="requirements.txt file not found"): + parse_requirements_txt( + "requires-python:>=3.8\nrequirements-txt:nonexistent.txt\n" + ) + + +@pytest.fixture +def run_create_venv_with_pthfile(tmp_path): + + def inner(requirements_content): + req_file = tmp_path / "requirements.txt" + req_file.write_text(requirements_content) + + requirements = MachEnvRequirements() + requirements.requirements_txt_files.append( + RequirementsTxtSpecifier(str(req_file)) + ) + + venv_root = tmp_path / "venv" + external_python = ExternalPythonSite(sys.executable) + metadata = MozSiteMetadata( + sys.hexversion, + "test", + SitePackagesSource.VENV, + external_python, + str(venv_root), + ) + + venv = PythonVirtualenv(str(venv_root)) + _create_venv_with_pthfile(venv, [], True, requirements, metadata) + + return venv + + return inner + + +def test_requirements_txt_install_requires_hashes( + monkeypatch, run_create_venv_with_pthfile +): + monkeypatch.delenv("MACH_SHOW_PIP_OUTPUT", raising=False) + + try: + run_create_venv_with_pthfile("certifi==2021.5.30\n") + pytest.fail("Expected CalledProcessError to be raised due to missing hashes") + except subprocess.CalledProcessError as e: + error_output = e.stderr if e.stderr else "" + assert ( + "hash" in error_output.lower() + ), f"Expected hash error in stderr, got: {error_output}" + + +def test_requirements_txt_installs_with_hashes(run_create_venv_with_pthfile): + requirements_content = ( + "certifi==2021.5.30 " + "--hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8\n" + ) + + venv = run_create_venv_with_pthfile(requirements_content) + + site_packages = venv.resolve_sysconfig_packages_path("purelib") + assert os.path.exists(site_packages), f"site-packages not found: {site_packages}" + + certifi_path = os.path.join(site_packages, "certifi") + assert os.path.exists(certifi_path), f"certifi package not found in {site_packages}" + + if __name__ == "__main__": main() diff --git a/python/mozbuild/mozbuild/test/python.toml b/python/mozbuild/mozbuild/test/python.toml @@ -113,6 +113,8 @@ subsuite = "mozbuild" ["test_rewrite_mozbuild.py"] +["test_site_dependency_extractor.py"] + ["test_telemetry.py"] ["test_telemetry_settings.py"] diff --git a/python/mozbuild/mozbuild/test/test_site_dependency_extractor.py b/python/mozbuild/mozbuild/test/test_site_dependency_extractor.py @@ -0,0 +1,75 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import pytest +from mozunit import main + +from mozbuild.lockfiles.site_dependency_extractor import ( + DependencyParseError, + SiteDependencyExtractor, +) + + +def test_requirements_txt_parsing(tmp_path): + sites_dir = tmp_path / "sites" + sites_dir.mkdir() + + requirements_txt = tmp_path / "requirements.txt" + requirements_txt.write_text( + """# Comment line +certifi==2021.5.30 --hash=sha256:abc123 + +# Another comment +urllib3==1.26.5 --hash=sha256:def456 +requests==2.26.0 --hash=sha256:ghi789 + +# Empty line follows + +-r other-requirements.txt +--index-url https://pypi.org/simple +""" + ) + + site_file = sites_dir / "test-site.txt" + site_file.write_text( + """requires-python:>=3.8 +requirements-txt:requirements.txt +""" + ) + + extractor = SiteDependencyExtractor("test-site", sites_dir, tmp_path) + requires_python, dependencies = extractor.parse() + + assert requires_python == ">=3.8" + assert len(dependencies) == 3 + + dep_names = {dep.name for dep in dependencies} + assert "certifi" in dep_names + assert "urllib3" in dep_names + assert "requests" in dep_names + + dep_dict = {dep.name: dep.version for dep in dependencies} + assert dep_dict["certifi"] == "==2021.5.30 --hash=sha256" + assert dep_dict["urllib3"] == "==1.26.5 --hash=sha256" + assert dep_dict["requests"] == "==2.26.0 --hash=sha256" + + +def test_requirements_txt_missing_file(tmp_path): + sites_dir = tmp_path / "sites" + sites_dir.mkdir() + + site_file = sites_dir / "test-site.txt" + site_file.write_text( + """requires-python:>=3.8 +requirements-txt:nonexistent.txt +""" + ) + + extractor = SiteDependencyExtractor("test-site", sites_dir, tmp_path) + with pytest.raises(DependencyParseError, match="requirements.txt file not found"): + extractor.parse() + + +if __name__ == "__main__": + main()