From e56be322f360a4abe4e922403e30e472f56f592e Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:05:47 +0800 Subject: [PATCH 1/7] fix: remove most asserts --- src/bugit_v2/apps/app.py | 3 +- .../bug_report_submitters/jira_submitter.py | 30 +++++--- .../launchpad_submitter.py | 51 ++++++++----- .../local_file_submitter.py | 11 ++- .../bug_report_submitters/mock_jira.py | 21 ++++-- src/bugit_v2/bug_report_submitters/mock_lp.py | 24 ++++--- src/bugit_v2/checkbox_utils/checkbox_exec.py | 3 +- .../checkbox_utils/submission_extractor.py | 5 +- src/bugit_v2/components/confirm_dialog.py | 3 +- src/bugit_v2/components/file_picker.py | 3 +- src/bugit_v2/dut_utils/log_collectors.py | 45 ++++++------ src/bugit_v2/models/app_state.py | 72 ++++++++++++------- src/bugit_v2/screens/bug_report_screen.py | 37 ++++++---- src/bugit_v2/screens/job_selection_screen.py | 3 +- .../screens/recover_from_autosave_screen.py | 6 +- .../screens/session_selection_screen.py | 8 ++- .../screens/submission_progress_screen.py | 12 ++-- src/bugit_v2/utils/async_subprocess.py | 15 ++-- 18 files changed, 224 insertions(+), 128 deletions(-) diff --git a/src/bugit_v2/apps/app.py b/src/bugit_v2/apps/app.py index f79ce899..8ff18e38 100644 --- a/src/bugit_v2/apps/app.py +++ b/src/bugit_v2/apps/app.py @@ -387,7 +387,8 @@ def main( ): sudo_devmode_check() ensure_all_directories_exist() - assert ctx.command.name in ("lp", "jira", "local") + if ctx.command.name not in ("lp", "jira", "local"): + raise RuntimeError(f"Unexpected command name: {ctx.command.name!r}") if checkbox_submission: print(f"Decompressing checkbox submission at {checkbox_submission}") diff --git a/src/bugit_v2/bug_report_submitters/jira_submitter.py b/src/bugit_v2/bug_report_submitters/jira_submitter.py index 4295a6c9..32aaa7c0 100644 --- a/src/bugit_v2/bug_report_submitters/jira_submitter.py +++ b/src/bugit_v2/bug_report_submitters/jira_submitter.py @@ -174,7 +174,8 @@ def project_exists(self, project_name: str) -> None: https://hostname.com/jira/software/c/projects/NAME :return: true if the project exists """ - assert self.jira, "Jira client is not initialized" + if not self.jira: + raise JiraSubmitterError("Jira client is not initialized") try: self.jira.project(id=project_name) except Exception: @@ -186,7 +187,8 @@ def assignee_exists_and_unique(self, assignee: str) -> str: :param assignee: the email of the assignee or some form of ID :return: exists and unique """ - assert self.jira, "Jira client is not initialized" + if not self.jira: + raise JiraSubmitterError("Jira client is not initialized") query_result = self.jira.search_users(query=assignee) if len(query_result) == 0: @@ -198,7 +200,8 @@ def assignee_exists_and_unique(self, assignee: str) -> str: return query_result[0].accountId # pyright: ignore[reportAny] def all_components_exist(self, project: str, components: Sequence[str]) -> None: - assert self.jira, "Jira client is not initialized" + if not self.jira: + raise JiraSubmitterError("Jira client is not initialized") # the @translate_args decorator confuses the type checker query_result = cast(list[Component], self.jira.project_components(project)) for wanted_component in components: @@ -214,7 +217,8 @@ def all_components_exist(self, project: str, components: Sequence[str]) -> None: @override def bug_exists(self, bug_id: str) -> bool: - assert self.auth + if not self.auth: + raise JiraSubmitterError("Missing auth credentials") try: if not self.jira: @@ -273,8 +277,10 @@ def submit( bug_dict["description"] = content_str - assert self.auth, "Missing auth credentials" - assert JIRA_SERVER_ADDRESS, "JIRA_SERVER is not specified!" + if not self.auth: + raise JiraSubmitterError("Missing auth credentials") + if not JIRA_SERVER_ADDRESS: + raise JiraSubmitterError("JIRA_SERVER is not specified!") yield "Starting Jira authentication..." self.jira = JIRA( @@ -330,8 +336,10 @@ def get_cached_credentials(self) -> JiraBasicAuth | None: def upload_attachment( self, attachment_file: Path, filename: str | None = None ) -> None: - assert self.jira - assert self.issue + if not self.jira: + raise JiraSubmitterError("Jira client is not initialized") + if not self.issue: + raise JiraSubmitterError("Nothing has been submitted to Jira yet") # .add_attachment has a decorator that confuses the typechecker # go to its definition to see the expected arguments self.jira.add_attachment(self.issue.id, str(attachment_file), filename) @@ -339,8 +347,10 @@ def upload_attachment( @property @override def bug_url(self) -> str: - assert self.jira - assert self.issue, "Nothing has been submitted to Jira yet" + if not self.jira: + raise JiraSubmitterError("Jira client is not initialized") + if not self.issue: + raise JiraSubmitterError("Nothing has been submitted to Jira yet") return f"{self.jira.server_url}/browse/{self.issue.key}" @override diff --git a/src/bugit_v2/bug_report_submitters/launchpad_submitter.py b/src/bugit_v2/bug_report_submitters/launchpad_submitter.py index cc140579..9982d14a 100644 --- a/src/bugit_v2/bug_report_submitters/launchpad_submitter.py +++ b/src/bugit_v2/bug_report_submitters/launchpad_submitter.py @@ -35,7 +35,11 @@ ) LP_APP_NAME = os.getenv("BUGIT_APP_NAME", "bugit-v2") -assert SERVICE_ROOT in VALID_SERVICE_ROOTS +if SERVICE_ROOT not in VALID_SERVICE_ROOTS: + raise RuntimeError( + "Invalid APPORT_LAUNCHPAD_INSTANCE, " + f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" + ) @final @@ -167,11 +171,13 @@ def on_mount(self): @work(thread=True) def main_auth_sequence(self): - assert SERVICE_ROOT in VALID_SERVICE_ROOTS, ( - "Invalid APPORT_LAUNCHPAD_INSTANCE, " - f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" - ) - assert LP_APP_NAME, "BUGIT_APP_NAME was not specified" + if SERVICE_ROOT not in VALID_SERVICE_ROOTS: + raise RuntimeError( + "Invalid APPORT_LAUNCHPAD_INSTANCE, " + f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" + ) + if not LP_APP_NAME: + raise RuntimeError("BUGIT_APP_NAME was not specified") log_widget = self.query_exactly_one("#lp_login_stdout", RichLog) auth_engine = GraphicalAuthorizeRequestTokenWithURL( @@ -244,7 +250,8 @@ class LaunchpadSubmitter(BugReportSubmitter[Path]): lp_bug_object: Any | None = None # TODO: make a wrapper for this def check_project_existence(self, project_name: str) -> Any: - assert self.lp_client + if not self.lp_client: + raise RuntimeError("Launchpad client is not initialized") try: # type checker freaks out here # since launchpad lib wants unknown member access + index access @@ -259,7 +266,8 @@ def check_project_existence(self, project_name: str) -> Any: raise ValueError(error_message) def check_assignee_existence(self, assignee: str) -> Any: - assert self.lp_client + if not self.lp_client: + raise RuntimeError("Launchpad client is not initialized") try: return self.lp_client.people[ # pyright: ignore[reportIndexIssue, reportOptionalSubscript, reportUnknownVariableType] assignee @@ -269,7 +277,8 @@ def check_assignee_existence(self, assignee: str) -> Any: raise ValueError(error_message) def check_series_existence(self, series: str) -> Any: - assert self.lp_client + if not self.lp_client: + raise RuntimeError("Launchpad client is not initialized") try: return self.lp_client.project.getSeries( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess, reportUnknownVariableType] name=series @@ -286,12 +295,15 @@ def bug_exists(self, bug_id: str) -> bool: def submit( self, bug_report: BugReport ) -> Generator[str | AdvanceMessage, None, None]: - assert SERVICE_ROOT in VALID_SERVICE_ROOTS, ( - "Invalid APPORT_LAUNCHPAD_INSTANCE, " - f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" - ) - assert LP_APP_NAME, "BUGIT_APP_NAME was not specified" - assert LP_AUTH_FILE_PATH.exists(), "At this point auth should already be valid" + if SERVICE_ROOT not in VALID_SERVICE_ROOTS: + raise RuntimeError( + "Invalid APPORT_LAUNCHPAD_INSTANCE, " + f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" + ) + if not LP_APP_NAME: + raise RuntimeError("BUGIT_APP_NAME was not specified") + if not LP_AUTH_FILE_PATH.exists(): + raise RuntimeError("At this point auth should already be valid") yield f"Logging into Launchpad: {SERVICE_ROOT}" try: @@ -368,7 +380,8 @@ def submit( bug_report.project # index access also has a side effect ], ) - assert self.lp_bug_object, "Unexpected null bug" + if not self.lp_bug_object: + raise RuntimeError("Unexpected null bug") # https://documentation.ubuntu.com/launchpad/user/explanation/launchpad-api/launchpadlib/#persistent-references-to-launchpad-objects yield AdvanceMessage( f"Created bug: {self.lp_bug_object!s}" # pyright: ignore[reportUnknownArgumentType] @@ -399,7 +412,8 @@ def submit( def upload_attachment( self, attachment_file: Path, filename: str | None = None ) -> str | None: - assert self.lp_bug_object, "No launchpad bug has been created or fetched" + if not self.lp_bug_object: + raise RuntimeError("No launchpad bug has been created or fetched") with open(attachment_file, "rb") as f: # this might explode on low memory systems # but idk how to work around it @@ -412,7 +426,8 @@ def upload_attachment( @property @override def bug_url(self) -> str: - assert self.lp_bug_object, "No launchpad bug has been created or fetched" + if not self.lp_bug_object: + raise RuntimeError("No launchpad bug has been created or fetched") match SERVICE_ROOT: case "production": return f"{LPNET_WEB_ROOT}bugs/{self.lp_bug_object.id}" diff --git a/src/bugit_v2/bug_report_submitters/local_file_submitter.py b/src/bugit_v2/bug_report_submitters/local_file_submitter.py index a1d590f6..3a3d20f0 100644 --- a/src/bugit_v2/bug_report_submitters/local_file_submitter.py +++ b/src/bugit_v2/bug_report_submitters/local_file_submitter.py @@ -109,12 +109,14 @@ def get_cached_credentials(self) -> None: @property @override def bug_url(self) -> str: - assert self.archive_path, "Report archive not created" + if not self.archive_path: + raise FileNotFoundError("Report archive not created") return str(self.archive_path) @override def finalize(self) -> str: - assert self.archive_name, "Unexpected call before archive name was determined" + if not self.archive_name: + raise RuntimeError("Unexpected call before archive name was determined") working_dir = Path(self.working_dir.name) @@ -126,7 +128,10 @@ def finalize(self) -> str: format="gztar", ) ) - assert self.archive_path.exists() + if not self.archive_path.exists(): + raise FileNotFoundError( + f"Archive {self.archive_path} was not created as expected" + ) self.finalize_ok = True return f"The bug report archive is at {self.archive_path}" except Exception as e: diff --git a/src/bugit_v2/bug_report_submitters/mock_jira.py b/src/bugit_v2/bug_report_submitters/mock_jira.py index 702c75be..8c182951 100644 --- a/src/bugit_v2/bug_report_submitters/mock_jira.py +++ b/src/bugit_v2/bug_report_submitters/mock_jira.py @@ -54,7 +54,8 @@ def project_exists(self, project_name: str) -> None: https://hostname.com/jira/software/c/projects/NAME :return: true if the project exists """ - assert self.jira, "Jira object is not initialized" + if not self.jira: + raise JiraSubmitterError("Jira object is not initialized") try: self.jira.project(id=project_name) except Exception: @@ -66,7 +67,8 @@ def assignee_exists_and_unique(self, assignee: str) -> None: :param assignee: the email of the assignee or some form of ID :return: exists and unique """ - assert self.jira, "Jira object is not initialized" + if not self.jira: + raise JiraSubmitterError("Jira object is not initialized") query_result = self.jira.search_users(query=assignee) if len(query_result) == 0: @@ -75,7 +77,8 @@ def assignee_exists_and_unique(self, assignee: str) -> None: raise JiraSubmitterError(f"Assignee '{assignee}' isn't unique!") def all_components_exist(self, project: str, components: Sequence[str]) -> None: - assert self.jira, "Jira object is not initialized" + if not self.jira: + raise JiraSubmitterError("Jira object is not initialized") # the @translate_args decorator confuses the type checker query_result = cast(list[Component], self.jira.project_components(project)) for wanted_component in components: @@ -89,7 +92,8 @@ def all_components_exist(self, project: str, components: Sequence[str]) -> None: @override def bug_exists(self, bug_id: str) -> bool: - assert self.auth + if not self.auth: + raise JiraSubmitterError("Missing auth credentials") try: if not self.jira: @@ -123,8 +127,10 @@ def submit( "issuetype": {"name": "Bug"}, } - assert self.auth, "Missing auth credentials" - assert JIRA_SERVER_ADDRESS, "JIRA_SERVER is not specified!" + if not self.auth: + raise JiraSubmitterError("Missing auth credentials") + if not JIRA_SERVER_ADDRESS: + raise JiraSubmitterError("JIRA_SERVER is not specified!") yield "Starting Jira authentication..." self.jira = JIRA( @@ -190,7 +196,8 @@ def upload_attachment( @property @override def bug_url(self) -> str: - assert self.mock_issue + if not self.mock_issue: + raise JiraSubmitterError("Nothing has been submitted to Jira yet") return "http://example.com/" @override diff --git a/src/bugit_v2/bug_report_submitters/mock_lp.py b/src/bugit_v2/bug_report_submitters/mock_lp.py index 8ae139aa..21f04cae 100644 --- a/src/bugit_v2/bug_report_submitters/mock_lp.py +++ b/src/bugit_v2/bug_report_submitters/mock_lp.py @@ -40,7 +40,8 @@ class MockLaunchpadSubmitter(BugReportSubmitter[Path]): auth_modal = LaunchpadAuthModal def check_project_existence(self, project_name: str) -> Any: - assert self.lp_client + if not self.lp_client: + raise RuntimeError("Launchpad client is not initialized") try: # type checker freaks out here # since launchpad lib wants unknown member access + index access @@ -55,7 +56,8 @@ def check_project_existence(self, project_name: str) -> Any: raise ValueError(error_message) def check_assignee_existence(self, assignee: str) -> Any: - assert self.lp_client + if not self.lp_client: + raise RuntimeError("Launchpad client is not initialized") try: return self.lp_client.people[ # pyright: ignore[reportIndexIssue, reportOptionalSubscript, reportUnknownVariableType] assignee @@ -65,7 +67,8 @@ def check_assignee_existence(self, assignee: str) -> Any: raise ValueError(error_message) def check_series_existence(self, series: str) -> Any: - assert self.lp_client + if not self.lp_client: + raise RuntimeError("Launchpad client is not initialized") try: return self.lp_client.project.getSeries( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess, reportUnknownVariableType] name=series @@ -83,12 +86,15 @@ def submit( self, bug_report: BugReport ) -> Generator[str | AdvanceMessage, None, None]: - assert SERVICE_ROOT in VALID_SERVICE_ROOTS, ( - "Invalid APPORT_LAUNCHPAD_INSTANCE, " - f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" - ) - assert LP_APP_NAME, "BUGIT_APP_NAME was not specified" - assert LP_AUTH_FILE_PATH.exists(), "At this point auth should already be valid" + if SERVICE_ROOT not in VALID_SERVICE_ROOTS: + raise RuntimeError( + "Invalid APPORT_LAUNCHPAD_INSTANCE, " + f"expected one of {VALID_SERVICE_ROOTS}, but got {SERVICE_ROOT}" + ) + if not LP_APP_NAME: + raise RuntimeError("BUGIT_APP_NAME was not specified") + if not LP_AUTH_FILE_PATH.exists(): + raise RuntimeError("At this point auth should already be valid") yield f"Logging into Launchpad: {SERVICE_ROOT}" try: diff --git a/src/bugit_v2/checkbox_utils/checkbox_exec.py b/src/bugit_v2/checkbox_utils/checkbox_exec.py index 5aad4501..94462dbe 100644 --- a/src/bugit_v2/checkbox_utils/checkbox_exec.py +++ b/src/bugit_v2/checkbox_utils/checkbox_exec.py @@ -45,7 +45,8 @@ async def checkbox_exec( :return: whatever subprocess.run returns """ checkbox_info = get_checkbox_info() - assert checkbox_info, "Unable to find checkbox on this DUT" + if not checkbox_info: + raise RuntimeError("Unable to find checkbox on this DUT") logger.info(f"Checkbox args: {checkbox_args}") if additional_env: diff --git a/src/bugit_v2/checkbox_utils/submission_extractor.py b/src/bugit_v2/checkbox_utils/submission_extractor.py index 620a20a2..ada45faf 100644 --- a/src/bugit_v2/checkbox_utils/submission_extractor.py +++ b/src/bugit_v2/checkbox_utils/submission_extractor.py @@ -11,7 +11,10 @@ def read_simple_submission(submission_path: Path) -> SimpleCheckboxSubmission: with tarfile.open(submission_path, "r:xz") as f: json_io_reader = f.extractfile("submission.json") - assert json_io_reader, f"submission.json does not exist in {submission_path}" + if not json_io_reader: + raise FileNotFoundError( + f"submission.json does not exist in {submission_path}" + ) return SimpleCheckboxSubmission( submission_path.absolute(), BaseSimpleCheckboxSubmission.model_validate( diff --git a/src/bugit_v2/components/confirm_dialog.py b/src/bugit_v2/components/confirm_dialog.py index 96027c9f..769fa9a6 100644 --- a/src/bugit_v2/components/confirm_dialog.py +++ b/src/bugit_v2/components/confirm_dialog.py @@ -52,5 +52,6 @@ def on_mount(self) -> None: self.query_exactly_one(f"#{self.focus_id_on_mount}", Button).focus() def on_button_pressed(self, event: Button.Pressed) -> None: - assert event.button.id + if not event.button.id: + raise RuntimeError("Button pressed without an id") self.dismiss(cast(T, event.button.id)) diff --git a/src/bugit_v2/components/file_picker.py b/src/bugit_v2/components/file_picker.py index f5856b5c..5e8c132d 100644 --- a/src/bugit_v2/components/file_picker.py +++ b/src/bugit_v2/components/file_picker.py @@ -174,7 +174,8 @@ def on_mount(self): @on(Button.Pressed, ".delete_selection") def delete_file_from_list(self, event: Button.Pressed): - assert event.button.name is not None + if event.button.name is None: + raise RuntimeError("Delete button is missing its file name") try: self._chosen_files.remove(Path(event.button.name)) self.post_message(self.FilesUpdated(list(self._chosen_files))) diff --git a/src/bugit_v2/dut_utils/log_collectors.py b/src/bugit_v2/dut_utils/log_collectors.py index 4d7b5734..00284e56 100644 --- a/src/bugit_v2/dut_utils/log_collectors.py +++ b/src/bugit_v2/dut_utils/log_collectors.py @@ -69,9 +69,8 @@ class LogCollector: async def pack_checkbox_session( target_dir: Path, bug_report: BugReport, _on_output: Callable[[str], None] | None ) -> str: - assert ( - bug_report.checkbox_session is not None - ), "Can't use this collector if there's no checkbox session" + if bug_report.checkbox_session is None: + raise RuntimeError("Can't use this collector if there's no checkbox session") shutil.make_archive( str(target_dir / "checkbox_session"), @@ -193,13 +192,13 @@ async def snap_debug( async def pack_checkbox_submission( target_dir: Path, bug_report: BugReport, _on_output: Callable[[str], None] | None ): - assert ( - bug_report.checkbox_submission is not None - ), "Can't use this collector if there's no checkbox submission" + if bug_report.checkbox_submission is None: + raise RuntimeError("Can't use this collector if there's no checkbox submission") submission_path = bug_report.checkbox_submission.submission_path - assert ( - submission_path.exists() - ), f"{submission_path} was deleted after the bug report was created!" + if not submission_path.exists(): + raise RuntimeError( + f"{submission_path} was deleted after the bug report was created!" + ) shutil.copyfile(submission_path, target_dir / os.path.basename(submission_path)) @@ -213,19 +212,21 @@ async def long_job_outputs( Only used when the job's stdout is way too long for the description """ - assert ( - bug_report.checkbox_session is not None - ), "Can't use this collector if there's no checkbox session" - assert bug_report.job_id is not None, "Can't use this collector if there's no job id" - assert ( - bug_report.checkbox_session.session_path.exists() - ), f"{bug_report.checkbox_session.session_path} was deleted after the bug report was created!" + if bug_report.checkbox_session is None: + raise RuntimeError("Can't use this collector if there's no checkbox session") + if bug_report.job_id is None: + raise RuntimeError("Can't use this collector if there's no job id") + if not bug_report.checkbox_session.session_path.exists(): + raise RuntimeError( + f"{bug_report.checkbox_session.session_path} was deleted after the bug report was created!" + ) job_output = bug_report.checkbox_session.get_job_output(bug_report.job_id) - assert ( - job_output - ), "This collector should not be called if there's no job output. Please report this bug to bugit's repo." + if not job_output: + raise RuntimeError( + "This collector should not be called if there's no job output. Please report this bug to bugit's repo." + ) added_keys: list[str] = [] for k, v in job_output.items(): @@ -242,7 +243,8 @@ async def long_job_outputs( async def oem_getlogs( target_dir: Path, _: BugReport, on_output: Callable[[str], None] | None ): - assert target_dir.exists(), f"Target directory {target_dir} does not exist" + if not target_dir.exists(): + raise RuntimeError(f"Target directory {target_dir} does not exist") dump_coef_path = Path("/sys/module/snd_hda_codec/parameters/dump_coef") if dump_coef_path.exists(): try: @@ -275,7 +277,8 @@ async def sosreport( ): if host_is_ubuntu_core(): raise RuntimeError("SOS Report cannot run on ubuntu core") - assert target_dir.exists(), f"Target directory {target_dir} does not exist" + if not target_dir.exists(): + raise RuntimeError(f"Target directory {target_dir} does not exist") await asp_check_call( [ *NSENTER_PREFIX, diff --git a/src/bugit_v2/models/app_state.py b/src/bugit_v2/models/app_state.py index 5ae07c15..eb9a6a11 100644 --- a/src/bugit_v2/models/app_state.py +++ b/src/bugit_v2/models/app_state.py @@ -60,7 +60,8 @@ def __init__(self, context: AppContext | None = None) -> None: @property def context(self) -> AppContext: - assert self._context, "Use before context is assigned" + if not self._context: + raise RuntimeError("Use before context is assigned") return self._context @context.setter @@ -115,7 +116,10 @@ def go_forward(self, screen_result: object) -> AppState: return JobSelectionState(self.context) # recover from existing report path - assert isinstance(screen_result, SerializableBugReport) + if not isinstance(screen_result, SerializableBugReport): + raise TypeError( + f"Unexpected screen result during autosave recovery: {screen_result!r}" + ) backup = screen_result.to_bug_report() self.context.bug_report_init_state = backup self.context.session = backup.checkbox_session or NullSelection.NO_SESSION @@ -143,17 +147,16 @@ def c(): class SessionSelectionState(AppState): @override def assertions(self) -> None: - assert self.context.session in ( - None, - NullSelection.NO_SESSION, - ), "Entered session selection with one already selected" - assert self.context.job_id in ( - None, - NullSelection.NO_JOB, - ), f"Impossible to have a job ID during session selection: {self.context.job_id}" - assert ( - self.context.bug_report_to_submit is None - ), "Impossible to have a complete bug report during session selection" + if self.context.session not in (None, NullSelection.NO_SESSION): + raise RuntimeError("Entered session selection with one already selected") + if self.context.job_id not in (None, NullSelection.NO_JOB): + raise RuntimeError( + f"Impossible to have a job ID during session selection: {self.context.job_id}" + ) + if self.context.bug_report_to_submit is not None: + raise RuntimeError( + "Impossible to have a complete bug report during session selection" + ) @override def go_back(self) -> "AppState | None": @@ -186,12 +189,15 @@ def get_screen_constructor(self): class JobSelectionState(AppState): @override def assertions(self) -> None: - assert isinstance(self.context.session, AbstractCheckboxSession) or isinstance( - self.context.checkbox_submission, SimpleCheckboxSubmission - ), "No source to choose jobs from" - assert ( - self.context.bug_report_to_submit is None - ), "Impossible to have a complete bug report during job selection" + if not ( + isinstance(self.context.session, AbstractCheckboxSession) + or isinstance(self.context.checkbox_submission, SimpleCheckboxSubmission) + ): + raise TypeError("No source to choose jobs from") + if self.context.bug_report_to_submit is not None: + raise RuntimeError( + "Impossible to have a complete bug report during job selection" + ) @override def go_back(self) -> "AppState | None": @@ -209,7 +215,10 @@ def go_back(self) -> "AppState | None": @override def go_forward(self, screen_result: object) -> AppState: # can either go to job selection or editor - assert (type(screen_result) is str) or (screen_result is NullSelection.NO_JOB) + if not ((type(screen_result) is str) or (screen_result is NullSelection.NO_JOB)): + raise RuntimeError( + f"Unexpected return value from job selection: {screen_result!r}" + ) self.context.job_id = screen_result return ReportEditorState(self.context) @@ -251,9 +260,10 @@ def get_screen_constructor(self): class ReportEditorState(AppState): @override def assertions(self) -> None: - assert ( - self.context.bug_report_to_submit is None - ), "Impossible to have a complete bug report during job selection" + if self.context.bug_report_to_submit is not None: + raise RuntimeError( + "Impossible to have a complete bug report during job selection" + ) @override def go_back(self) -> "AppState | None": @@ -312,7 +322,10 @@ def go_back(self) -> "AppState | None": @override def go_forward(self, screen_result: object) -> AppState: - assert isinstance(screen_result, BugReport) + if not isinstance(screen_result, BugReport): + raise TypeError( + f"Unexpected screen result from report editor: {screen_result!r}" + ) self.context.bug_report_to_submit = screen_result return SubmissionProgressState(self.context) @@ -335,7 +348,8 @@ def get_screen_constructor(self): class SubmissionProgressState(AppState): @override def assertions(self) -> None: - assert self.context.bug_report_to_submit, "No bug report to submit" + if not self.context.bug_report_to_submit: + raise RuntimeError("No bug report to submit") @override def go_back(self) -> "AppState | None": @@ -343,7 +357,10 @@ def go_back(self) -> "AppState | None": @override def go_forward(self, screen_result: object) -> AppState: - assert screen_result in RETURN_SCREEN_CHOICES + if screen_result not in RETURN_SCREEN_CHOICES: + raise RuntimeError( + f"Unexpected screen result from submission progress: {screen_result!r}" + ) backup = self.context.bug_report_to_submit self.context.bug_report_to_submit = None # this is where we get the select a new session/job buttons @@ -367,7 +384,8 @@ def go_forward(self, screen_result: object) -> AppState: @override def get_screen_constructor(self): def c(): - assert self.context.bug_report_to_submit + if not self.context.bug_report_to_submit: + raise RuntimeError("No bug report to submit") return SubmissionProgressScreen( self.context.bug_report_to_submit, self.context.submitter(), diff --git a/src/bugit_v2/screens/bug_report_screen.py b/src/bugit_v2/screens/bug_report_screen.py index a9ac2c50..5acca48d 100644 --- a/src/bugit_v2/screens/bug_report_screen.py +++ b/src/bugit_v2/screens/bug_report_screen.py @@ -476,7 +476,10 @@ def compose(self) -> ComposeResult: yield t else: for k, output in job_output.items(): - assert type(output) is str + if type(output) is not str: + raise RuntimeError( + f"Job output for {k} is not a string: {output!r}" + ) t = TextArea( output, read_only=True, classes="ha default_box mh75" ) @@ -709,9 +712,10 @@ def watch_validation_status(self): btn.label = "Submit Bug Report" def _standard_info_worker_callback(self, event: Worker.StateChanged): - assert ( - event.worker.is_finished - ), "Standard info callback invoked but the worker has not finished" + if not event.worker.is_finished: + raise RuntimeError( + "Standard info callback invoked but the worker has not finished" + ) textarea = self.query_exactly_one( f"#{BugReportElemId.DESCRIPTION}", DescriptionEditor @@ -793,10 +797,12 @@ def _standard_info_worker_callback(self, event: Worker.StateChanged): ) def _get_cert_status_worker_callback(self, event: Worker.StateChanged): - assert self.job_id is not NullSelection.NO_JOB - assert ( - event.worker.is_finished - ), "Cert status callback invoked but the worker has not finished" + if self.job_id is NullSelection.NO_JOB: + raise RuntimeError("Cert status callback invoked without a job id") + if not event.worker.is_finished: + raise RuntimeError( + "Cert status callback invoked but the worker has not finished" + ) cert_status_box = self.query_exactly_one("#cert_status_box", Label) @@ -815,13 +821,18 @@ def _get_cert_status_worker_callback(self, event: Worker.StateChanged): self._color_cert_status_box(cert_status and cert_status.cert_status) def _get_submission_cert_status_worker_callback(self, event: Worker.StateChanged): - assert self.job_id is not NullSelection.NO_JOB - assert ( - event.worker.is_finished - ), "Submission cert status callback invoked but the worker has not finished" + if self.job_id is NullSelection.NO_JOB: + raise RuntimeError( + "Submission cert status callback invoked without a job id" + ) + if not event.worker.is_finished: + raise RuntimeError( + "Submission cert status callback invoked but the worker has not finished" + ) if event.worker.state == WorkerState.SUCCESS: - assert event.worker.result in CERT_STATUSES + if event.worker.result not in CERT_STATUSES: + raise RuntimeError(f"Unexpected cert status: {event.worker.result!r}") self._color_cert_status_box(event.worker.result) else: logger.error(f"Cert status worker error {event.worker.error}") diff --git a/src/bugit_v2/screens/job_selection_screen.py b/src/bugit_v2/screens/job_selection_screen.py index 4ca66a4f..0e3ba67f 100644 --- a/src/bugit_v2/screens/job_selection_screen.py +++ b/src/bugit_v2/screens/job_selection_screen.py @@ -90,7 +90,8 @@ def on_mount(self) -> None: @on(Button.Pressed, "#continue_button") def finish_selection(self) -> None: - assert self.selected_job is not None + if self.selected_job is None: + raise RuntimeError("Continue button pressed without a selected job") if self.selected_job == "bugit_no_job": self.dismiss(NullSelection.NO_JOB) else: diff --git a/src/bugit_v2/screens/recover_from_autosave_screen.py b/src/bugit_v2/screens/recover_from_autosave_screen.py index be830ac5..99171241 100644 --- a/src/bugit_v2/screens/recover_from_autosave_screen.py +++ b/src/bugit_v2/screens/recover_from_autosave_screen.py @@ -171,7 +171,8 @@ async def handle_buttons(self, event: Button.Pressed): self.dismiss(None) return - assert event.button.name + if not event.button.name: + raise RuntimeError("Button pressed without a name") if event.button.name.startswith("delete:"): savefile_name = event.button.name.removeprefix("delete:") @@ -185,7 +186,8 @@ async def handle_buttons(self, event: Button.Pressed): self.dismiss(self.valid_autosave_data[event.button.name]) def _button_text(self, filename: str) -> Content: - assert filename in self.valid_autosave_data + if filename not in self.valid_autosave_data: + raise RuntimeError(f"Unknown autosave filename: {filename!r}") autosave = self.valid_autosave_data[filename] lines: list[str] = [] if self.is_relative: diff --git a/src/bugit_v2/screens/session_selection_screen.py b/src/bugit_v2/screens/session_selection_screen.py index ac378db2..b12e59f3 100644 --- a/src/bugit_v2/screens/session_selection_screen.py +++ b/src/bugit_v2/screens/session_selection_screen.py @@ -79,13 +79,15 @@ def action_refresh_sessions(self): def on_button_pressed(self, event: Button.Pressed) -> None: session_path = event.button.name try: - assert session_path + if not session_path: + raise RuntimeError("Button pressed without a session path") if session_path == "bugit_no_session" and event.button.tooltip is not None: self.dismiss(NullSelection.NO_SESSION) else: - assert Path(session_path).exists() + if not Path(session_path).exists(): + raise RuntimeError(f"{session_path} does not exist") self.dismiss(Path(session_path).absolute()) - except AssertionError: + except RuntimeError: self.app.notify( "Was it deleted while BugIt is running?", title=f"{session_path} doesn't exist.", diff --git a/src/bugit_v2/screens/submission_progress_screen.py b/src/bugit_v2/screens/submission_progress_screen.py index 4937f23c..5fcbd0cb 100644 --- a/src/bugit_v2/screens/submission_progress_screen.py +++ b/src/bugit_v2/screens/submission_progress_screen.py @@ -162,7 +162,8 @@ async def on_mount(self) -> None: auth_rv = await self.app.push_screen_wait( self.submitter.auth_modal() ) - assert auth_rv + if not auth_rv: + raise RuntimeError("Auth modal was dismissed without a result") ( self.submitter.auth, self.submitter.allow_cache_credentials, @@ -354,7 +355,8 @@ def check_if_worker_is_pending(name: LogName): ) def start_parallel_attachment_upload(self) -> None: - assert self.activity_log_widget + if not self.activity_log_widget: + raise RuntimeError("Activity log widget is not mounted") progress_bar = self.query_exactly_one("#progress", ProgressBar) def upload_one(f: Path): @@ -395,7 +397,8 @@ def upload_one(f: Path): self._log_with_time(f"Uploading: {file.name}") def start_sequential_attachment_upload(self) -> None: - assert self.activity_log_widget + if not self.activity_log_widget: + raise RuntimeError("Activity log widget is not mounted") progress_bar = self.query_exactly_one("#progress", ProgressBar) def upload_all(): @@ -439,7 +442,8 @@ def upload_all(): def create_bug(self) -> None: """Do the entire bug creation sequence. This should be run in a worker""" - assert self.activity_log_widget + if not self.activity_log_widget: + raise RuntimeError("Activity log widget is not mounted") progress_bar = self.query_exactly_one("#progress", ProgressBar) display_name = self.submitter.display_name or self.submitter.name diff --git a/src/bugit_v2/utils/async_subprocess.py b/src/bugit_v2/utils/async_subprocess.py index 2c21d372..99ca2ecc 100644 --- a/src/bugit_v2/utils/async_subprocess.py +++ b/src/bugit_v2/utils/async_subprocess.py @@ -95,8 +95,10 @@ async def asp_check_output( *cmd, stdout=asp.PIPE, stderr=asp.PIPE, cwd=cwd ) - assert proc.stdout is not None - assert proc.stderr is not None + if proc.stdout is None: + raise RuntimeError("Subprocess stdout pipe was not created") + if proc.stderr is None: + raise RuntimeError("Subprocess stderr pipe was not created") # bind to locals so type checkers can narrow away the `| None` from # `proc.stdout`/`proc.stderr` inside the nested closure below stdout_stream = proc.stdout @@ -131,7 +133,8 @@ async def _run() -> tuple[bytes, bytes]: proc.kill() raise e - assert proc.returncode is not None + if proc.returncode is None: + raise RuntimeError("Subprocess finished without a return code") if proc.returncode != 0: raise CalledProcessError(proc.returncode, cmd, stdout, stderr) @@ -185,7 +188,8 @@ async def asp_check_call( async def _run() -> int: if streaming: - assert proc.stdout is not None + if proc.stdout is None: + raise RuntimeError("Subprocess stdout pipe was not created") # capture=False: asp_check_call doesn't return captured stdout, # so don't hold potentially huge output in memory for nothing await _stream_lines(proc.stdout, on_line, dest_file, capture=False) @@ -252,7 +256,8 @@ async def asp_run( proc.kill() raise e - assert proc.returncode is not None + if proc.returncode is None: + raise RuntimeError("Subprocess finished without a return code") return sp.CompletedProcess[str]( cmd, proc.returncode, stdout.decode(), stderr.decode() From 678fc44b3ca964a38859240adf62c20de2fc2992 Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:11:15 +0800 Subject: [PATCH 2/7] perf: strip more debug code --- snap/snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 838b6e4a..4e6981d0 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -49,6 +49,7 @@ environment: PYTHONPATH: "$PYTHONPATH:$SNAP/usr/lib/python3/dist-packages" # prevent lstopo from hanging on 26.04 HWLOC_COMPONENTS: "-gl" + PYTHONOPTIMIZE: '1' layout: # this combined with the 'cp' in the install hook From ba3247e387ecbf1318f301dde3461bbdcd576b59 Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:58:38 +0800 Subject: [PATCH 3/7] fix: combined err --- src/bugit_v2/screens/submission_progress_screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bugit_v2/screens/submission_progress_screen.py b/src/bugit_v2/screens/submission_progress_screen.py index 5fcbd0cb..88cbd086 100644 --- a/src/bugit_v2/screens/submission_progress_screen.py +++ b/src/bugit_v2/screens/submission_progress_screen.py @@ -314,7 +314,7 @@ def stream_line(line: str) -> None: ] ) ) - logger.error(repr(e)) + logger.error(f"{collector.display_name}:{e!r}") if collector.manual_collection_command: self._log_collector( f"You can rerun [blue]{collector.display_name}[/] " From 06783c1daf91c334644ee7d0c1e4e385a2679af0 Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:59:29 +0800 Subject: [PATCH 4/7] style: text --- src/bugit_v2/screens/submission_progress_screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bugit_v2/screens/submission_progress_screen.py b/src/bugit_v2/screens/submission_progress_screen.py index 88cbd086..638f2239 100644 --- a/src/bugit_v2/screens/submission_progress_screen.py +++ b/src/bugit_v2/screens/submission_progress_screen.py @@ -833,7 +833,7 @@ def _actually_finish(self): ) except Exception as e: finalize_ok = False - self._log_with_time(f"[red]ERR when finalizing[/]: {e!r}") + self._log_with_time(f"[red]FINALIZE FAIL[/]: {e!r}") logger.error(e) finish_message_lines = ["[green]Submission finished![/]"] From db7a7a978cfea8edbf650941a5e09875465412d7 Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:03:39 +0800 Subject: [PATCH 5/7] fix: convert final asserts --- src/bugit_v2/screens/bug_report_screen.py | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/bugit_v2/screens/bug_report_screen.py b/src/bugit_v2/screens/bug_report_screen.py index 5acca48d..232cd727 100644 --- a/src/bugit_v2/screens/bug_report_screen.py +++ b/src/bugit_v2/screens/bug_report_screen.py @@ -853,12 +853,25 @@ def _build_bug_report(self) -> BugReport: ).pressed_button # shouldn't fail at runtime, major logic error if they do - assert selected_severity_button - assert selected_severity_button.name in SEVERITIES - assert selected_issue_file_time_button - assert selected_issue_file_time_button.name in ISSUE_FILE_TIMES - assert selected_status_button - assert selected_status_button.name in BUG_STATUSES + if not selected_severity_button: + raise RuntimeError("No severity button selected") + if selected_severity_button.name not in SEVERITIES: + raise RuntimeError( + f"Unexpected severity button name: {selected_severity_button.name!r}" + ) + if not selected_issue_file_time_button: + raise RuntimeError("No issue file time button selected") + if selected_issue_file_time_button.name not in ISSUE_FILE_TIMES: + raise RuntimeError( + "Unexpected issue file time button name: " + + f"{selected_issue_file_time_button.name!r}" + ) + if not selected_status_button: + raise RuntimeError("No status button selected") + if selected_status_button.name not in BUG_STATUSES: + raise RuntimeError( + f"Unexpected status button name: {selected_status_button.name!r}" + ) hidden_collectors: list[LogName] = [] if self.job_output_too_long: From 1d9b178de6ed36aa8e191968b0edf061a1c6867f Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:05:41 +0800 Subject: [PATCH 6/7] fix: link err types --- src/bugit_v2/screens/submission_progress_screen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bugit_v2/screens/submission_progress_screen.py b/src/bugit_v2/screens/submission_progress_screen.py index 638f2239..bcac75b6 100644 --- a/src/bugit_v2/screens/submission_progress_screen.py +++ b/src/bugit_v2/screens/submission_progress_screen.py @@ -163,7 +163,7 @@ async def on_mount(self) -> None: self.submitter.auth_modal() ) if not auth_rv: - raise RuntimeError("Auth modal was dismissed without a result") + raise ValueError("Auth modal was dismissed without a result") ( self.submitter.auth, self.submitter.allow_cache_credentials, @@ -180,7 +180,7 @@ async def on_mount(self) -> None: # overwrite the old one to avoid counting th_log_with_time time waiting # for the auth modal self.progress_start_time = time.time() - except AssertionError: + except ValueError: if self.mode == "screen": prompt = ConfirmScreen[ReturnScreenChoice]( "[red]Authentication form returned nothing[/]", From f1579de28d3d33970efa3eea7fb4a2400c98669d Mon Sep 17 00:00:00 2001 From: Zhongning Li <60045212+tomli380576@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:08:33 +0800 Subject: [PATCH 7/7] fix: copilot checks --- src/bugit_v2/checkbox_utils/submission_extractor.py | 3 ++- src/bugit_v2/models/app_state.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bugit_v2/checkbox_utils/submission_extractor.py b/src/bugit_v2/checkbox_utils/submission_extractor.py index ada45faf..6021859b 100644 --- a/src/bugit_v2/checkbox_utils/submission_extractor.py +++ b/src/bugit_v2/checkbox_utils/submission_extractor.py @@ -10,10 +10,11 @@ def read_simple_submission(submission_path: Path) -> SimpleCheckboxSubmission: with tarfile.open(submission_path, "r:xz") as f: + # .extractfile raises KeyError if the file is not in the tar json_io_reader = f.extractfile("submission.json") if not json_io_reader: raise FileNotFoundError( - f"submission.json does not exist in {submission_path}" + f"submission.json exists, but it's not a regular file in {submission_path}" ) return SimpleCheckboxSubmission( submission_path.absolute(), diff --git a/src/bugit_v2/models/app_state.py b/src/bugit_v2/models/app_state.py index eb9a6a11..c8262e59 100644 --- a/src/bugit_v2/models/app_state.py +++ b/src/bugit_v2/models/app_state.py @@ -262,7 +262,7 @@ class ReportEditorState(AppState): def assertions(self) -> None: if self.context.bug_report_to_submit is not None: raise RuntimeError( - "Impossible to have a complete bug report during job selection" + "Impossible to have a complete bug report during report editing" ) @override