class LaunchpadTestCase(unit.TestCase): def setUp(self): super().setUp() self._project = self._make_snapcraft_project() self.lpc = LaunchpadClient( project=self._project, build_id="id", user="******", architectures=[] ) @mock.patch("launchpadlib.launchpad.Launchpad.login_with") def test_login(self, mock_login): self.lpc.login() self.assertThat(self.lpc.user, Equals("user")) mock_login.assert_called_with( "snapcraft remote-build {}".format(snapcraft.__version__), "production", mock.ANY, credentials_file=mock.ANY, version="devel", ) @mock.patch("launchpadlib.launchpad.Launchpad.login_with") def test_login_connection_issues(self, mock_login): mock_login.side_effect = ConnectionRefusedError() self.assertRaises(errors.LaunchpadHttpsError, self.lpc.login) mock_login.side_effect = TimeoutError() self.assertRaises(errors.LaunchpadHttpsError, self.lpc.login) @mock.patch("snapcraft.internal.remote_build.LaunchpadClient.login") def test_load_connection_issues(self, mock_login): self.lpc._lp = LaunchpadImpl() # ConnectionRefusedError should surface. self.lpc._lp._load_mock.side_effect = ConnectionRefusedError self.assertRaises(ConnectionRefusedError, self.lpc._lp_load_url, "foo") # Load URL should work OK after single connection reset. self.lpc._lp._load_mock.side_effect = [ConnectionResetError, None] self.lpc._lp_load_url(url="foo") mock_login.assert_called() # Load URL should fail with two connection resets. self.lpc._lp._load_mock.side_effect = [ ConnectionResetError, ConnectionResetError, ] self.assertRaises(ConnectionResetError, self.lpc._lp_load_url, "foo") mock_login.assert_called() def test_create_snap(self): self.lpc._lp = LaunchpadImpl() self.lpc._create_snap() self.lpc._lp.snaps.new_mock.assert_called_with( auto_build=False, auto_build_archive="/ubuntu/+archive/primary", auto_build_pocket="Updates", git_path="master", git_repository_url="https://[email protected]/~user/+git/id/", name="id", owner="/~user", ) def test_create_snap_with_archs(self): self.lpc._lp = LaunchpadImpl() self.lpc.architectures = ["arch1", "arch2"] self.lpc._create_snap() self.lpc._lp.snaps.new_mock.assert_called_with( auto_build=False, auto_build_archive="/ubuntu/+archive/primary", auto_build_pocket="Updates", git_path="master", git_repository_url="https://[email protected]/~user/+git/id/", name="id", owner="/~user", processors=["/+processors/arch1", "/+processors/arch2"], ) def test_delete_snap(self): self.lpc._lp = LaunchpadImpl() self.lpc._delete_snap() self.lpc._lp.snaps.getByName_mock.assert_called_with(name="id", owner="/~user") def test_start_build(self): self.lpc._lp = LaunchpadImpl() self.lpc.start_build() @mock.patch( "tests.unit.remote_build.test_launchpad.SnapImpl.requestBuilds", return_value=SnapBuildReqImpl( status="Failed", error_message="snapcraft.yaml not found..." ), ) def test_start_build_error(self, mock_rb): self.lpc._lp = LaunchpadImpl() raised = self.assertRaises(errors.RemoteBuilderError, self.lpc.start_build) self.assertThat(str(raised), Contains("snapcraft.yaml not found...")) @mock.patch( "tests.unit.remote_build.test_launchpad.SnapImpl.requestBuilds", return_value=SnapBuildReqImpl(status="Pending", error_message=""), ) def test_start_build_error_timeout(self, mock_rb): self.lpc._lp = LaunchpadImpl() raised = self.assertRaises( errors.RemoteBuilderNotReadyError, self.lpc.start_build, timeout=0 ) self.assertThat(str(raised), Contains("is not ready")) @mock.patch("snapcraft.internal.remote_build.LaunchpadClient._download_file") def test_monitor_build(self, mock_download_file): open("test_i386.txt", "w").close() open("test_i386.1.txt", "w").close() self.lpc._lp = LaunchpadImpl() self.lpc.start_build() self.lpc.monitor_build(interval=0) mock_download_file.assert_has_calls( [ mock.call(url="url_for/snap_file_i386.snap", dst="snap_file_i386.snap"), mock.call( url="url_for/build_log_file_1", gunzip=True, dst="test_i386.2.txt" ), mock.call( url="url_for/build_log_file_2", gunzip=True, dst="test_amd64.txt" ), ] ) @mock.patch("snapcraft.internal.remote_build.LaunchpadClient._download_file") @mock.patch( "tests.unit.remote_build.test_launchpad.BuildImpl.getFileUrls", return_value=[] ) @mock.patch("logging.Logger.error") def test_monitor_build_error(self, mock_log, mock_urls, mock_download_file): self.lpc._lp = LaunchpadImpl() self.lpc.start_build() self.lpc.monitor_build(interval=0) mock_download_file.assert_has_calls( [ mock.call( url="url_for/build_log_file_2", gunzip=True, dst="test_amd64.txt" ) ] ) mock_log.assert_called_with("Build failed for arch 'amd64'.") def test_get_build_status(self): self.lpc._lp = LaunchpadImpl() self.lpc.start_build() build_status = self.lpc.get_build_status() self.assertThat( build_status, Equals({"i386": "Successfully built", "amd64": "Failed to build"}), ) def _make_snapcraft_project(self): yaml = textwrap.dedent( """\ name: test base: core18 version: "1.0" summary: test description: test confinement: strict grade: stable """ ) snapcraft_yaml_file_path = self.make_snapcraft_yaml(yaml) project = snapcraft.project.Project( snapcraft_yaml_file_path=snapcraft_yaml_file_path ) return project def test_git_repository_creation(self): source_testdir = self.useFixture(TestDir()) source_testdir.create_file("foo") repo_dir = source_testdir.path self.assertFalse(os.path.exists(os.path.join(repo_dir, ".git"))) self.lpc._gitify_repository(repo_dir) self.assertTrue(os.path.exists(os.path.join(repo_dir, ".git"))) @mock.patch("snapcraft.internal.sources.Git.push", return_value=None) def test_push_source_tree(self, mock_push): self.lpc._lp = LaunchpadImpl() source_testdir = self.useFixture(TestDir()) source_testdir.create_file("foo") repo_dir = source_testdir.path self.lpc.push_source_tree(repo_dir) mock_push.assert_called_with( "https://*****:*****@git.launchpad.net/~user/+git/id/", "HEAD:master", force=True, ) @mock.patch( "snapcraft.internal.sources.Git.push", side_effect=SnapcraftPullError( command="git push HEAD:master https://user:access-token@url", exit_code=128 ), ) def test_push_source_tree_error(self, mock_push): self.lpc._lp = LaunchpadImpl() source_testdir = self.useFixture(TestDir()) source_testdir.create_file("foo") repo_dir = source_testdir.path self.assertRaises( errors.LaunchpadGitPushError, self.lpc.push_source_tree, repo_dir ) mock_push.assert_called_with( "https://*****:*****@git.launchpad.net/~user/+git/id/", "HEAD:master", force=True, )
def remote_build( recover: int, status: int, build_on: str, launchpad_accept_public_upload: bool, launchpad_user: str, package_all_sources: bool, echoer=echo, ) -> None: """Dispatch a snap for remote build. Command remote-build sends the current project to be built remotely. After the build is complete, packages for each architecture are retrieved and will be available in the local filesystem. If not specified in the snapcraft.yaml file, the list of architectures to build can be set using the --build-on option. If both are specified, an error will occur. Interrupted remote builds can be resumed using the --recover option, followed by the build number informed when the remote build was originally dispatched. The current state of the remote build for each architecture can be checked using the --status option. \b Examples: snapcraft remote-build --launchpad-user <user> snapcraft remote-build --launchpad-user <user> --build-on=amd64 snapcraft remote-build --launchpad-user <user> --build-on=amd64,arm64,armhf,i386,ppc64el,s390x snapcraft remote-build --launchpad-user <user> --recover 47860738 snapcraft remote-build --launchpad-user <user> --status 47860738 """ if not launchpad_accept_public_upload: raise errors.AcceptPublicUploadError() echo.warning( "snapcraft remote-build is experimental and is subject to change - use with caution." ) project = get_project() # TODO: use project.is_legacy() when available. base = project.info.get_build_base() if base is None: raise errors.BaseRequiredError() # Use a hash of current working directory to distinguish between other # potential project builds occurring in parallel elsewhere. project_hash = project._get_project_directory_hash() build_id = f"snapcraft-{project.info.name}-{project_hash}" architectures = _determine_architectures(project, build_on) lp = LaunchpadClient( project=project, build_id=build_id, user=launchpad_user, architectures=architectures, ) lp.login() if status: _print_status(lp) elif recover: # Recover from interrupted build. if not lp.has_outstanding_build(): echo.info("No build found.") return echo.info("Recovering build...") _monitor_build(lp) elif lp.has_outstanding_build(): # There was a previous build that hasn't finished. # Recover from interrupted build. echo.info("Found previously started build.") _print_status(lp) if not echo.confirm("Do you wish to recover this build?", default=True): _clean_build(lp) _start_build( lp=lp, project=project, build_id=build_id, package_all_sources=package_all_sources, ) _monitor_build(lp) else: _start_build( lp=lp, project=project, build_id=build_id, package_all_sources=package_all_sources, ) _monitor_build(lp)