def test_stable_grade_for_stable_base(self): lifecycle.execute(steps.PULL, self.project_config) global_state = states.GlobalState.load( filepath=self.global_state_filepath) self.assertThat(global_state.get_required_grade(), Equals("stable")) self.fake_storeapi_get_info.mock.assert_called_once_with("core18")
def test_prime_with_build_info_records_snapcraft_yaml(self): self.useFixture( fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_INFO", "1")) project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: test-part: plugin: nil """), snap_type="type: app", ) lifecycle.execute(steps.PRIME, project_config) expected = textwrap.dedent("""\ name: test base: core18 version: "1.0" summary: test description: test confinement: strict grade: stable type: app parts: test-part: plugin: nil """) self.assertThat( os.path.join(steps.PRIME.name, "snap", "snapcraft.yaml"), FileContains(expected), )
def test_grade_not_queried_for_if_already_set(self): # Set the grade global_state = states.GlobalState() global_state.set_required_grade("devel") global_state.save(filepath=self.global_state_filepath) lifecycle.execute(steps.PULL, self.project_config) self.fake_storeapi_get_info.mock.assert_not_called()
def test_stable_grade_for_non_stable_base(self): self.fake_storeapi_get_info.mock.side_effect = SnapNotFoundError( snap_name="core18") lifecycle.execute(steps.PULL, self.project_config) global_state = states.GlobalState.load( filepath=self.global_state_filepath) self.assertThat(global_state.get_required_grade(), Equals("devel")) self.fake_storeapi_get_info.mock.assert_called_once_with("core18")
def test_clean_removes_global_state(self): project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: test-part: plugin: nil """)) lifecycle.execute(steps.PULL, project_config) lifecycle.clean(project_config.project, parts=None) self.assertThat(os.path.join("snap", ".snapcraft"), Not(DirExists()))
def test_prime_excludes_internal_snapcraft_dir(self): project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: test-part: plugin: nil """)) lifecycle.execute(steps.PRIME, project_config) self.assertThat(os.path.join(steps.PRIME.name, "snap", ".snapcraft"), Not(DirExists()))
def test_prime_without_build_info_does_not_record(self): self.useFixture( fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_INFO", None)) project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: test-part: plugin: nil """)) lifecycle.execute(steps.PRIME, project_config) for file_name in ("snapcraft.yaml", "manifest.yaml"): self.assertThat(os.path.join(steps.PRIME.name, "snap", file_name), Not(FileExists()))
def test_pull_is_dirty_if_target_arch_changes( self, mock_install_build_snaps, mock_install_build_packages, mock_enable_cross_compilation, ): # Set the option to error on dirty/outdated steps with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( snapcraft_legacy.config.OutdatedStepAction.ERROR) mock_install_build_packages.return_value = [] project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: part1: plugin: nil """)) project = Project( snapcraft_yaml_file_path=self.snapcraft_yaml_file_path, target_deb_arch="amd64", ) project_config = project_loader.load_config(project) # Pull it with amd64 lifecycle.execute(steps.PULL, project_config) # Reset logging since we only care about the following self.fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(self.fake_logger) project = Project( snapcraft_yaml_file_path=self.snapcraft_yaml_file_path, target_deb_arch="armhf", ) project_config = project_loader.load_config(project) # Pull it again with armhf. Should catch that the part needs to be # re-pulled due to the change in target architecture and raise an # error. raised = self.assertRaises(errors.StepOutdatedError, lifecycle.execute, steps.PULL, project_config) self.assertThat(self.fake_logger.output, Contains("Setting target machine to 'armhf'")) self.assertThat(raised.step, Equals(steps.PULL)) self.assertThat(raised.part, Equals("part1")) self.assertThat( raised.report, Equals("The 'deb_arch' project option appears to have changed.\n"), )
def test_has_step_run(self): # No steps should have run, yet main_part = self.project_config.parts.get_part("main") self.assertFalse(self.cache.has_step_run(main_part, steps.PULL)) # Now run the pull step lifecycle.execute(steps.PULL, self.project_config, part_names=["main"]) # Should still have cached that no steps have run self.assertFalse(self.cache.has_step_run(main_part, steps.PULL)) # Now clear that step from the cache, and it should be up-to-date self.cache.clear_step(main_part, steps.PULL) self.assertTrue(self.cache.has_step_run(main_part, steps.PULL))
def test_clean_leaves_prime_alone_for_tried(self, mock_for_root): project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: test-part: plugin: nil """)) lifecycle.execute(steps.PRIME, project_config) lifecycle.clean(project_config.project, parts=None) self.assertThat( steps.PRIME.name, DirExists(), "Expected prime directory to remain after cleaning for tried snap", )
def test_dirty_build_raises(self, mock_install_build_snaps): # Set the option to error on dirty/outdated steps with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( snapcraft_legacy.config.OutdatedStepAction.ERROR) project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: part1: plugin: nil """)) # Build it. lifecycle.execute(steps.BUILD, project_config) # Reset logging since we only care about the following self.fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(self.fake_logger) def _fake_dirty_report(self, step): if step == steps.BUILD: return pluginhandler.DirtyReport( dirty_properties={"foo", "bar"}) return None # Should catch that the part needs to be rebuilt and raise an error. with mock.patch.object(pluginhandler.PluginHandler, "get_dirty_report", _fake_dirty_report): raised = self.assertRaises(errors.StepOutdatedError, lifecycle.execute, steps.BUILD, project_config) self.assertThat(self.fake_logger.output, Equals("Skipping pull part1 (already ran)\n")) self.assertThat(raised.step, Equals(steps.BUILD)) self.assertThat(raised.part, Equals("part1")) self.assertThat( raised.report, Equals( "The 'bar' and 'foo' part properties appear to have changed.\n" ), ) self.assertThat(raised.parts_names, Equals("part1"))
def test_non_prime_and_no_version(self): snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path, version=None) snapcraft_yaml.data["adopt-info"] = "test-part" snapcraft_yaml.update_part( "test-part", { "plugin": "nil", "override-build": "snapcraftctl set-version 1.0" }, ) self.useFixture(snapcraft_yaml) project = Project( snapcraft_yaml_file_path=snapcraft_yaml.snapcraft_yaml_file_path) project_config = project_loader.load_config(project) # This should not fail lifecycle.execute(steps.PULL, project_config)
def test_dependency_is_staged_when_required(self, mock_install_build_snaps): project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: part1: plugin: nil part2: plugin: nil after: - part1 """)) lifecycle.execute(steps.PULL, project_config, part_names=["part2"]) self.assertThat( self.fake_logger.output, Contains("'part2' has dependencies that need to be staged: part1"), )
def test_get_dirty_report(self): # No dirty reports should be available, yet dependent_part = self.project_config.parts.get_part("dependent") self.assertFalse( self.cache.get_dirty_report(dependent_part, steps.PULL)) # Now run the pull step lifecycle.execute(steps.PULL, self.project_config) # Re-stage main, which will make dependent dirty lifecycle.execute(steps.PULL, self.project_config, part_names=["main"]) # Should still have cached that it's not dirty, though self.assertFalse( self.cache.get_dirty_report(dependent_part, steps.PULL)) # Now clear that step from the cache, and it should be up-to-date self.cache.clear_step(dependent_part, steps.PULL) self.assertTrue(self.cache.get_dirty_report(dependent_part, steps.PULL))
def test_no_exception_when_dependency_is_required_but_already_staged( self, mock_install_build_snaps): project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: part1: plugin: nil part2: plugin: nil after: - part1 """)) def _fake_should_step_run(self, step, force=False): return self.name != "part1" with mock.patch.object(pluginhandler.PluginHandler, "should_step_run", _fake_should_step_run): lifecycle.execute(steps.PULL, project_config, part_names=["part2"]) self.assertThat(self.fake_logger.output, Contains("Pulling part2")) self.assertThat(self.fake_logger.output, Not(Contains("Pulling part1")))
def test_get_outdated_report(self): # No outdated reports should be available, yet main_part = self.project_config.parts.get_part("main") self.assertFalse(self.cache.get_outdated_report(main_part, steps.PULL)) # Now run the pull step for main lifecycle.execute(steps.PULL, self.project_config, part_names=["main"]) # Change the source on disk, which will make the pull step of main # outdated (to ensure this is the case, manually set the timestamp) open("new-file", "w").close() pull_state_file = states.get_step_state_file(main_part.plugin.statedir, steps.PULL) access_time = os.stat(pull_state_file).st_atime modified_time = os.stat(pull_state_file).st_atime os.utime("new-file", (access_time, modified_time + 1)) # Should still have cached that it's not outdated, though self.assertFalse(self.cache.get_outdated_report(main_part, steps.PULL)) # Now clear that step from the cache, and it should be up-to-date self.cache.clear_step(main_part, steps.PULL) self.assertTrue(self.cache.get_outdated_report(main_part, steps.PULL))
def test_dirty_stage_part_with_built_dependent_raises(self): # Set the option to error on dirty/outdated steps with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( snapcraft_legacy.config.OutdatedStepAction.ERROR) project_config = self.make_snapcraft_project( textwrap.dedent("""\ parts: part1: plugin: nil part2: plugin: nil after: [part1] """)) # Stage dependency lifecycle.execute(steps.STAGE, project_config, part_names=["part1"]) # Build dependent lifecycle.execute(steps.BUILD, project_config, part_names=["part2"]) def _fake_dirty_report(self, step): if step == steps.STAGE: return pluginhandler.DirtyReport(dirty_properties={"foo"}, dirty_project_options={"bar"}) return None # Should stage no problem with mock.patch.object(pluginhandler.PluginHandler, "get_dirty_report", _fake_dirty_report): lifecycle.execute(steps.STAGE, project_config, part_names=["part1"]) # Reset logging since we only care about the following self.fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(self.fake_logger) # Should raise an error since part2 is now dirty raised = self.assertRaises(errors.StepOutdatedError, lifecycle.execute, steps.BUILD, project_config) output = self.fake_logger.output.split("\n") part1_output = [line.strip() for line in output if "part1" in line] self.assertThat(part1_output, Equals(["Skipping pull part1 (already ran)"])) self.assertThat(raised.step, Equals(steps.PULL)) self.assertThat(raised.part, Equals("part2")) self.assertThat(raised.report, Equals("A dependency has changed: 'part1'\n"))
def _execute( # noqa: C901 step: steps.Step, parts: Sequence[str], pack_project: bool = False, output: Optional[str] = None, shell: bool = False, shell_after: bool = False, setup_prime_try: bool = False, ua_token: Optional[str] = None, **kwargs, ) -> "Project": # Cleanup any previous errors. _clean_provider_error() build_provider = get_build_provider(**kwargs) build_provider_flags = get_build_provider_flags( build_provider, ua_token=ua_token, **kwargs ) apply_host_provider_flags(build_provider_flags) is_managed_host = build_provider == "managed-host" # Temporary fix to ignore target_arch. if kwargs.get("target_arch") is not None and build_provider in ["multipass", "lxd"]: echo.warning( "Ignoring '--target-arch' flag. This flag requires --destructive-mode and is unsupported with Multipass and LXD build providers." ) kwargs.pop("target_arch") project = get_project(is_managed_host=is_managed_host, **kwargs) conduct_project_sanity_check(project, **kwargs) project_path = pathlib.Path(project._project_dir) if project_path.name in ["build-aux", "snap"]: echo.warning( f"Snapcraft is running in directory {project_path.name!r}. If this is the snap assets directory, please run snapcraft from {project_path.parent}." ) if build_provider in ["host", "managed-host"]: with ua_manager.ua_manager(ua_token): project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack( project.prime_dir, compression=project._snap_meta.compression, output=output, ) else: build_provider_class = build_providers.get_provider_for(build_provider) try: build_provider_class.ensure_provider() except build_providers.errors.ProviderNotFound as provider_error: if provider_error.prompt_installable: if echo.is_tty_connected() and echo.confirm( "Support for {!r} needs to be set up. " "Would you like to do it now?".format(provider_error.provider) ): build_provider_class.setup_provider(echoer=echo) else: raise provider_error else: raise provider_error with build_provider_class( project=project, echoer=echo, build_provider_flags=build_provider_flags ) as instance: try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step, part_names=parts) elif pack_project: instance.pack_project(output=output) elif setup_prime_try: instance.expose_prime() instance.execute_step(step, part_names=parts) else: instance.execute_step(step, part_names=parts) except Exception: _retrieve_provider_error(instance) if project.debug: instance.shell() else: echo.warning( "Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure." ) raise else: if shell or shell_after: instance.shell() return project