def test_security_indicator_scoring_missing_secure( self, recommendation_type) -> None: """Make sure we don't accept package if si info is missing when recommendation is secure.""" flexmock(GraphDatabase) GraphDatabase.should_receive( "get_si_aggregated_python_package_version").with_args( package_name="flask", package_version="0.12.0", index_url="https://pypi.org/simple").and_raise( NotFoundError).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase(), stack_info=[]) context.recommendation_type = recommendation_type with pytest.raises(NotAcceptable): with SecurityIndicatorStep.assigned_context(context): step = SecurityIndicatorStep() step.run(None, package_version) assert len(context.stack_info) == 1 assert set(context.stack_info[0].keys()) == {"message", "type", "link"}
def test_remove_package_tuple_transitive_error(self) -> None: """Remove a transitive dependency which will cause error during removal.""" direct_dependencies = { ("flask", "0.12.1", "https://pypi.org/simple"): PackageVersion( name="flask", version="==0.12.1", index=Source("https://pypi.org/simple"), develop=False, ) } paths = { ("flask", "0.12.1", "https://pypi.org/simple"): [ (("flask", "0.12.1", "https://pypi.org/simple"), ("werkzeug", "0.13", "https://pypi.org/simple")), (("werkzeug", "0.13", "https://pypi.org/simple"), ("six", "1.0.0", "https://pypi.org/simple")), ] } step_context = StepContext.from_paths(direct_dependencies, paths=paths) with pytest.raises(CannotRemovePackage): with step_context.remove_package_tuples( ("six", "1.0.0", "https://pypi.org/simple")): pass
def test_not_resolved(self) -> None: direct_dependencies = { ("flask", "1.12.0", "https://pypi.org/simple"): PackageVersion( name="flask", version="==1.12.0", index="https://pypi.org/simple", develop=False, ) } paths = { ("flask", "1.12.0", "https://pypi.org/simple"): [(("flask", "1.12.0", "https://pypi.org/simple"), ("werkzeug", "0.13", None))] } step_context = StepContext.from_paths(direct_dependencies, paths) assert ("werkzeug", "0.13", None) in step_context.iter_transitive_dependencies_tuple() assert ("werkzeug", "0.13", None) in step_context.unsolved_packages assert step_context.unsolved_packages.get( ("werkzeug", "0.13", None)) is not None assert len( list(step_context.iter_transitive_dependencies(develop=None))) == 1 assert len( list( step_context.iter_transitive_dependencies(develop=False))) == 1 assert len( list(step_context.iter_transitive_dependencies(develop=True))) == 0 direct_dependencies = list(step_context.iter_direct_dependencies()) assert len(direct_dependencies) == 1 assert direct_dependencies[0].name == "flask"
def test_no_rule(self, context: Context) -> None: """Test if no rule is configured for the given package.""" package_version = PackageVersion( name="flask", version="==1.1.2", index=Source("https://pypi.org/simple"), develop=False) (GraphDatabase.should_receive( "get_python_package_version_solver_rules_all").with_args( "flask", "1.1.2", "https://pypi.org/simple", ).and_return([])) (GraphDatabase.should_receive( "get_python_package_version_solver_rules_all").with_args( "flask", "1.1.2", ).and_return([])) context.graph = GraphDatabase() assert not context.stack_info, "No stack info should be provided before test run" sieve = self.UNIT_TESTED() sieve.pre_run() with self.UNIT_TESTED.assigned_context(context): assert list(sieve.run( p for p in [package_version])) == [package_version] assert not context.stack_info, "No stack info should be provided by the pipeline unit"
def test_remove_package_tuple_transitive_with_direct_error(self) -> None: """Test removal of a package which does not have any candidate of direct dependency.""" paths = {} direct_dependencies = {} source = Source("https://pypi.org/simple") for version_identifier in ("0.12.1", "1.0.1"): package_tuple = ("flask", version_identifier, source.url) direct_dependencies[package_tuple] = PackageVersion( name="flask", version="==" + version_identifier, index=source.url, develop=False, ) paths[package_tuple] = [ (package_tuple, ("werkzeug", "0.13", "https://pypi.org/simple")), (package_tuple, ("werkzeug", "0.14", "https://pypi.org/simple")), (("werkzeug", "0.13", "https://pypi.org/simple"), ("six", "1.0.0", "https://pypi.org/simple")), (("werkzeug", "0.14", "https://pypi.org/simple"), ("six", "1.0.0", "https://pypi.org/simple")), ] step_context = StepContext.from_paths(direct_dependencies, paths) with pytest.raises(CannotRemovePackage): with step_context.remove_package_tuples( ("six", "1.0.0", "https://pypi.org/simple")): pass
def test_run_develop_not_match(self, context: Context, state: State, develop: bool) -> None: """Test not running the prescription if develop flag is set.""" prescription_str = f""" name: StepUnit type: step should_include: times: 1 adviser_pipeline: true match: package_version: name: numpy develop: {'true' if develop else 'false'} run: score: 0.5 """ prescription = yaml.safe_load(prescription_str) PRESCRIPTION_STEP_SCHEMA(prescription) StepPrescription.set_prescription(prescription) package_version = PackageVersion( name="numpy", version="==1.19.1", index=Source("https://thoth-station.ninja/simple"), develop=not develop, ) unit = StepPrescription() unit.pre_run() with unit.assigned_context(context): assert unit.run(state, package_version) is None
def test_cve_not_acceptable(self) -> None: """Test raising an exception if a secure software stack should be resolved.""" flexmock(GraphDatabase) GraphDatabase.should_receive("get_python_cve_records_all").with_args( package_name="flask", package_version="0.12.0").and_return([self._FLASK_CVE]).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase(), recommendation_type=RecommendationType.SECURITY, stack_info=[]) step = CvePenalizationStep() with CvePenalizationStep.assigned_context(context): assert not step._messages_logged with pytest.raises(NotAcceptable): step.run(None, package_version) assert len(step._messages_logged) == 1 assert ("flask", "0.12.0", "https://pypi.org/simple") in step._messages_logged assert len(context.stack_info) == 1 assert set(context.stack_info[0].keys()) == {"message", "link", "type"} assert self.verify_justification_schema(context.stack_info)
def test_cve_sieve_allow_cve(self, context: Context) -> None: """Make sure a CVE filtering allows allow-cve configuration.""" context.graph.should_receive("get_python_cve_records_all").with_args( package_name="flask", package_version="0.12.0").and_return([self._FLASK_CVE]) pv = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) assert not context.stack_info context.recommendation_type = RecommendationType.SECURITY context.labels["allow-cve"] = self._FLASK_CVE["cve_id"] with self.UNIT_TESTED.assigned_context(context): unit = self.UNIT_TESTED() unit.pre_run() result = list(unit.run((p for p in (pv, )))) assert result == [pv] assert context.stack_info == []
def test_run_log(self, caplog, context: Context, log_level: str) -> None: """Check logging messages.""" prescription_str = f""" name: SieveUnit type: sieve should_include: times: 1 adviser_pipeline: true match: package_version: name: flask version: '>1.0,<=1.1.0' index_url: 'https://pypi.org/simple' run: log: message: Some stack warning message printed by a sieve type: {log_level} """ prescription = yaml.safe_load(prescription_str) PRESCRIPTION_SIEVE_SCHEMA(prescription) SievePrescription.set_prescription(prescription) package_version = PackageVersion( name="flask", version="==1.1.0", index=Source("https://pypi.org/simple"), develop=False, ) self.check_run_log( caplog, context, log_level, SievePrescription, package_versions=(pv for pv in [package_version]) )
def test_cut_unsolved_error(self): """Test error cutting if dependency graph cannot be constructed.""" source = Source("https://pypi.org/simple") direct_dependencies = { ("flask", "0.12.1", source.url): PackageVersion( name="flask", version="==0.12.1", index=source, develop=False, ), } paths = { ("flask", "0.12.1", "https://pypi.org/simple"): [ (("flask", "0.12.1", "https://pypi.org/simple"), ("werkzeug", "0.15.5", None)), ], } step_context = StepContext.from_paths(direct_dependencies, paths) cut_unsolved = CutUnsolved( graph=None, project=None, library_usage=None, ) with pytest.raises(CannotRemovePackage): cut_unsolved.run(step_context)
def test_tf_avx2(self) -> None: """Test recommending TensorFlow with AVX2 support.""" package_version = PackageVersion( name="tensorflow", version="==2.2.0", develop=False, index=Source( "https://tensorflow.pypi.thoth-station.ninja/index/manylinux2010/AVX2/simple" ), ) # State and context are unused in the actual pipeline run. state, context = flexmock(), flexmock() with TensorFlowAVX2Step.assigned_context(context): unit = TensorFlowAVX2Step() result = unit.run(state, package_version) assert len(result[1]) == 1 assert result[1][0].pop( "link", None) is not None, "No link to justification document provided" assert result == ( 0.2, [{ "message": "AICoE TensorFlow builds are optimized for AVX2 instruction " "sets supported in the CPU identified", "type": "INFO", }], ) assert self.verify_justification_schema(result[1])
def test_no_cve_record(self, recommendation_type: RecommendationType) -> None: """Make sure no CVEs do not affect CVE scoring.""" flexmock(GraphDatabase) GraphDatabase.should_receive("get_python_cve_records_all").with_args( package_name="flask", package_version="0.12.0").and_return([]).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase(), recommendation_type=recommendation_type) with CvePenalizationStep.assigned_context(context): step = CvePenalizationStep() result = step.run(None, package_version) assert isinstance(result, tuple) assert len(result) == 2 assert result[0] == 0.0 assert result[1] == [{ "link": "https://thoth-station.ninja/j/no_cve", "message": "No known CVE known for 'flask' in version '0.12.0'", "package_name": "flask", "type": "INFO", }]
def test_pre_releases_disallowed_removal(self, context: Context, package_name: str, package_version: str) -> None: """Test no removals if pre-releases are allowed.""" pv = PackageVersion( name=package_name, version=f"=={package_version}", index=Source("https://pypi.org/simple"), develop=False, ) project = Project.from_strings(self._CASE_GLOBAL_DISALLOWED_PIPFILE) context.project = project sieve = self.UNIT_TESTED() sieve.update_configuration({ "package_name": None, "allow_prereleases": project.pipfile.thoth.allow_prereleases }) assert not context.stack_info with self.UNIT_TESTED.assigned_context(context): assert list(sieve.run(p for p in [pv])) == [] assert len(context.stack_info) == 1 assert self.verify_justification_schema(context.stack_info)
def test_remove_pre_releases_disallowed_noop(self, context: Context, package_name: str, package_version: str) -> None: """Test NOT removing dependencies based on pre-release configuration.""" pv = PackageVersion( name=package_name, version=f"=={package_version}", index=Source("https://pypi.org/simple"), develop=False, ) project = Project.from_strings(self._CASE_GLOBAL_DISALLOWED_PIPFILE) context.project = project sieve = self.UNIT_TESTED() sieve.update_configuration({ "package_name": None, "allow_prereleases": project.pipfile.thoth.allow_prereleases }) assert not context.stack_info with self.UNIT_TESTED.assigned_context(context): assert list(sieve.run(p for p in [pv])) == [pv] assert not context.stack_info, "No stack information should be provided"
def test_run_not_acceptable(self, context: Context, state: State) -> None: """Check raising not acceptable.""" prescription_str = """ name: StepUnit type: step should_include: times: 1 adviser_pipeline: true dependency_monkey_pipeline: true match: package_version: name: flask version: "~=0.0" index_url: "https://pypi.org/simple" run: not_acceptable: This is exception message reported """ prescription = yaml.safe_load(prescription_str) PRESCRIPTION_STEP_SCHEMA(prescription) StepPrescription.set_prescription(prescription) package_version = PackageVersion( name="flask", version="==0.12", index=Source("https://pypi.org/simple"), develop=False, ) self.check_run_not_acceptable(context, StepPrescription, state=state, package_version=package_version)
def test_run_stack_info(self, context: Context) -> None: """Check assigning stack info.""" prescription_str = """ name: SieveUnit type: sieve should_include: times: 1 adviser_pipeline: true match: package_version: name: flask version: '>1.0,<=1.1.0' index_url: 'https://pypi.org/simple' run: stack_info: - type: WARNING message: Some stack warning message printed by a sieve link: https://thoth-station.ninja """ prescription = yaml.safe_load(prescription_str) PRESCRIPTION_SIEVE_SCHEMA(prescription) SievePrescription.set_prescription(prescription) package_version = PackageVersion( name="flask", version="==1.1.0", index=Source("https://pypi.org/simple"), develop=False, ) self.check_run_stack_info(context, SievePrescription, package_versions=(pv for pv in [package_version]))
def test_run_stack_info(self, context: Context, state: State) -> None: """Check assigning stack info.""" prescription_str = """ name: StepUnit type: step should_include: times: 1 adviser_pipeline: true match: package_version: index_url: 'https://thoth-station.ninja/simple' run: stack_info: - type: WARNING message: Some message link: https://thoth-station.ninja """ prescription = yaml.safe_load(prescription_str) PRESCRIPTION_STEP_SCHEMA(prescription) StepPrescription.set_prescription(prescription) package_version = PackageVersion( name="flask", version="==1.1.0", index=Source("https://thoth-station.ninja/simple"), develop=False, ) self.check_run_stack_info(context, StepPrescription, state=state, package_version=package_version)
def test_indexes_in_meta(self): """Check indexes being adjusted when inserting a new package.""" package_version = PackageVersion( name="tensorflow", version="==1.9.0", develop=False, index=Source( "http://tensorflow.pypi.thoth-station.ninja/index/fedora28/jemalloc/simple/tensorflow/" ), ) project = Project.from_package_versions([package_version]) project_dict = project.to_dict() pipfile_dict = project_dict["requirements"] pipfile_lock_dict = project_dict["requirements_locked"] assert pipfile_lock_dict is None assert len(pipfile_dict["source"]) == 1 assert pipfile_dict["source"] == [{ "url": "http://tensorflow.pypi.thoth-station.ninja/index/fedora28/jemalloc/simple/tensorflow/", "verify_ssl": True, "name": "tensorflow-pypi-thoth-station-ninja", }]
def test_run_log(self, caplog, context: Context, state: State, log_level: str) -> None: """Check logging messages.""" prescription_str = f""" name: StepUnit type: step should_include: times: 1 adviser_pipeline: true match: package_version: name: flask run: log: message: Seen flask during resolution type: {log_level} """ prescription = yaml.safe_load(prescription_str) PRESCRIPTION_STEP_SCHEMA(prescription) StepPrescription.set_prescription(prescription) package_version = PackageVersion( name="flask", version="==1.1.0", index=Source("https://pypi.org/simple"), develop=False, ) self.check_run_log(caplog, context, log_level, StepPrescription, state=state, package_version=package_version)
def test_run_noop(self, context: Context, tf_versions: List[str]) -> None: """Test not adding a pseudonym for TensorFlow if no alternative releases found.""" unit = self.UNIT_TESTED() package_version = PackageVersion( name=unit.configuration["package_name"], version="==1.0.0", develop=False, index=Source("https://pypi.org/simple"), ) context.graph.should_receive( "get_solved_python_package_versions_all" ).with_args( package_name="tensorflow-gpu", package_version=None, index_url="https://pypi.org/simple", count=None, os_name=context.project.runtime_environment.operating_system.name, os_version=context.project.runtime_environment.operating_system. version, python_version=context.project.runtime_environment.python_version, distinct=True, is_missing=False, ).and_return(tf_versions).once() with unit.assigned_context(context): unit.pre_run() assert list(unit.run(package_version)) == []
def test_cve_penalization(self) -> None: """Make sure a CVE affects stack score.""" flexmock(GraphDatabase) GraphDatabase.should_receive("get_python_cve_records_all").with_args( package_name="flask", package_version="0.12.0").and_return([self._FLASK_CVE]).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase(), recommendation_type=RecommendationType.TESTING) with CvePenalizationStep.assigned_context(context): step = CvePenalizationStep() result = step.run(None, package_version) assert result is not None assert isinstance(result, tuple) and len(result) == 2 assert isinstance(result[0], float) assert result[0] == 1 * CvePenalizationStep.CONFIGURATION_DEFAULT[ "cve_penalization"] assert isinstance(result[1], list) assert result[1] == [self._FLASK_CVE] assert self.verify_justification_schema(result[1])
def test_os_sieve_no_error(self): """Test no error raised if no packages satisfy OS specific requirements.""" package_versions = [ PackageVersion( name="tensorflow", version="==1.9.0", index=Source( "https://tensorflow.pypi.thoth-station.ninja/index/fedora/30/jemalloc/simple/" ), develop=False, ) ] sieve_context = SieveContext.from_package_versions(package_versions) project = Project.from_strings( pipfile_str=self._PIPFILE_CONTENT_AICOE, runtime_environment=RuntimeEnvironment.from_dict( {"operating_system": { "name": "ubi", "version": "9" }})) os_sieve = OperatingSystemSieve(graph=None, project=project) os_sieve.run(sieve_context) assert set(sieve_context.iter_direct_dependencies_tuple()) == { ("tensorflow", "1.9.0", "https://tensorflow.pypi.thoth-station.ninja/index/fedora/30/jemalloc/simple/" ), }
def test_run_noop( self, context: Context, pv_version: str, pv_index_url: str, conf_version: str, conf_index_url: str ) -> None: """Test not adding a pseudonym for the given package.""" unit = self.UNIT_TESTED() unit.update_configuration( { "package_name": "tensorflow", "package_version": conf_version, "index_url": conf_index_url, "aliases": [ { "package_name": "intel-tensorflow", "package_version": "2.2.0", "index_url": "https://pypi.org/simple", }, { "package_name": "tensorflow-gpu", "package_version": "2.2.0", "index_url": "https://pypi.org/simple", }, { "package_name": "tensorflow", "package_version": "2.2.0", "index_url": "https://thoth-station.ninja/simple", }, ], } ) package_version = PackageVersion( name="tensorflow", version=pv_version, index=Source(pv_index_url), develop=False ) result = list(unit.run(package_version)) assert len(result) == 0
def test_security_indicator_scoring_missing_stable( self, recommendation_type) -> None: """Make sure package is kept even if no score exists for security indicators and add justification.""" flexmock(GraphDatabase) GraphDatabase.should_receive( "get_si_aggregated_python_package_version").with_args( package_name="flask", package_version="0.12.0", index_url="https://pypi.org/simple").and_raise( NotFoundError).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase()) context.recommendation_type = recommendation_type with SecurityIndicatorStep.assigned_context(context): step = SecurityIndicatorStep() result = step.run(None, package_version) assert result is not None assert isinstance(result, tuple) and len(result) == 2 assert result[0] == 0 assert len(result[1]) == 1 assert self.verify_justification_schema(result[1]) assert result[1][0]["type"] == "WARNING" assert (result[1][0]["message"] == "flask===0.12.0 on https://pypi.org/simple has no " "gathered information regarding security.")
def test_remove_package_tuple_transitive_with_direct_diamond_error( self) -> None: """Test removal of a package which does not have any candidate of direct dependency.""" direct_dependencies = { ("flask", "0.12.1", "https://pypi.org/simple"): PackageVersion( name="flask", version="==0.12.1", index=Source("https://pypi.org/simple"), develop=False, ) } paths = { ("flask", "0.12.1", "https://pypi.org/simple"): [ (("flask", "0.12.1", "https://pypi.org/simple"), ("werkzeug", "0.13", "https://pypi.org/simple")), (("werkzeug", "0.13", "https://pypi.org/simple"), ("six", "1.0.0", "https://pypi.org/simple")), (("flask", "0.12.1", "https://pypi.org/simple"), ("werkzeug", "0.14", "https://pypi.org/simple")), (("werkzeug", "0.14", "https://pypi.org/simple"), ("six", "1.0.0", "https://pypi.org/simple")), ], } step_context = StepContext.from_paths(direct_dependencies, paths) with pytest.raises(CannotRemovePackage): with step_context.remove_package_tuples( ("six", "1.0.0", "https://pypi.org/simple")): pass
def test_security_indicator_with_high_confidence(self) -> None: """Make sure we don't accept package if si info is missing when recommendation is secure.""" flexmock(GraphDatabase) GraphDatabase.should_receive( "get_si_aggregated_python_package_version").with_args( package_name="flask", package_version="0.12.0", index_url="https://pypi.org/simple").and_return( self._HIGH_HIGH_SECURITY_INFO).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase(), stack_info=[]) context.recommendation_type = RecommendationType.SECURITY with pytest.raises(NotAcceptable): with SecurityIndicatorStep.assigned_context(context): step = SecurityIndicatorStep() step.run(None, package_version) assert len(context.stack_info) == 1 assert self.verify_justification_schema(context.stack_info)
def _prepare_step_context() -> StepContext: """Prepare step context for test scenarios.""" direct_dependencies = {} paths = {} source = Source("https://pypi.org/simple") for version_identifier in ("0.12.1", "1.0.1"): package_tuple = ("flask", version_identifier, source.url) direct_dependencies[package_tuple] = PackageVersion( name="flask", version="==" + version_identifier, index=source.url, develop=False, ) paths[package_tuple] = [ (package_tuple, ("werkzeug", "0.13", "https://pypi.org/simple")), (package_tuple, ("werkzeug", "0.14", "https://pypi.org/simple")), (("werkzeug", "0.13", "https://pypi.org/simple"), ("six", "1.7.0", "https://pypi.org/simple")), (("werkzeug", "0.13", "https://pypi.org/simple"), ("six", "1.8.0", "https://pypi.org/simple")), (("werkzeug", "0.14", "https://pypi.org/simple"), ("six", "1.7.0", "https://pypi.org/simple")), (("werkzeug", "0.14", "https://pypi.org/simple"), ("six", "1.8.0", "https://pypi.org/simple")), ] return StepContext.from_paths(direct_dependencies, paths)
def test_security_indicator_scoring(self) -> None: """Make sure we do score security indicators when the info is available.""" flexmock(GraphDatabase) GraphDatabase.should_receive( "get_si_aggregated_python_package_version").with_args( package_name="flask", package_version="0.12.0", index_url="https://pypi.org/simple").and_return( self._SECURITY_INFO_EXISTS).once() package_version = PackageVersion( name="flask", version="==0.12.0", index=Source("https://pypi.org/simple"), develop=False, ) context = flexmock(graph=GraphDatabase()) context.recommendation_type = RecommendationType.STABLE with SecurityIndicatorStep.assigned_context(context): step = SecurityIndicatorStep() result = step.run(None, package_version) assert result is not None assert isinstance(result, tuple) and len(result) == 2 assert isinstance(result[0], float) assert self.verify_justification_schema(result[1])
def test_remove_all_transitive_error(self): """Test raising of an error if all the transitive deps of a type were removed.""" source = Source("https://pypi.org/simple") direct_dependencies = { ("tensorflow", "2.0.0", source.url): PackageVersion( name="tensorflow", version="==2.0.0", index=source, develop=False, ) } paths = { ("tensorflow", "2.0.0", "https://pypi.org/simple"): [ (("tensorflow", "2.0.0", "https://pypi.org/simple"), ("numpy", "1.0.0rc1", "https://thoth-station.ninja/simple")), ] } step_context = StepContext.from_paths(direct_dependencies, paths) project = Project.from_strings(self._CASE_PIPFILE) restrict_indexes = CutPreReleases( graph=None, project=project, library_usage=None, ) with pytest.raises(CannotRemovePackage): restrict_indexes.run(step_context)
def test_run_pseudonym(self, context: Context) -> None: """Test adding a pseudonym for TensorFlow.""" unit = self.UNIT_TESTED() package_version = PackageVersion( name=unit.configuration["package_name"], version="==1.0.0", develop=False, index=Source("https://pypi.org/simple"), ) context.graph.should_receive("get_solved_python_package_versions_all").with_args( package_name="tensorflow-gpu", package_version=None, index_url="https://pypi.org/simple", count=None, os_name=context.project.runtime_environment.operating_system.name, os_version=context.project.runtime_environment.operating_system.version, python_version=context.project.runtime_environment.python_version, distinct=True, is_missing=False, ).and_return( [ ("tensorflow-gpu", "1.0.0", "https://pypi.org/simple"), ("tensorflow-gpu", "2.0.0", "https://pypi.org/simple"), ] ).once() with unit.assigned_context(context): unit.pre_run() result = unit.run(package_version) assert list(result) == [("tensorflow-gpu", package_version.locked_version, package_version.index.url)]