From 2f867c28b4d16bbdab2ea70c68a7318f6ec670c9 Mon Sep 17 00:00:00 2001 From: Brandon Nguyen <112731698+bxngyn@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:50:44 -0400 Subject: [PATCH 01/11] vietnamese support --- submit50/locale/vi/LC_MESSAGES/submit50.po | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 submit50/locale/vi/LC_MESSAGES/submit50.po diff --git a/submit50/locale/vi/LC_MESSAGES/submit50.po b/submit50/locale/vi/LC_MESSAGES/submit50.po new file mode 100644 index 0000000..53c910a --- /dev/null +++ b/submit50/locale/vi/LC_MESSAGES/submit50.po @@ -0,0 +1,113 @@ +# Vietnamese translations for submit50. +# Copyright (C) 2025 ORGANIZATION +# This file is distributed under the same license as the submit50 project. +# FIRST AUTHOR , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: submit50 3.2.1\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2025-08-05 13:04-0400\n" +"PO-Revision-Date: 2025-08-05 13:06-0400\n" +"Last-Translator: FULL NAME \n" +"Language: vi\n" +"Language-Team: vi \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: submit50/__main__.py:71 +msgid "" +"Could not connect to submit.cs50.io.Please visit our status page " +"https://cs50.statuspage.io for more information." +msgstr "" +"Không kết nối đến submit.cs50.io được. Vui lòng xem " +"https://cs50.statuspage.io để biết thêm thông tin." + +#: submit50/__main__.py:135 +msgid "Files that will be submitted:" +msgstr "Các tập tin sẽ nộp:" + +#: submit50/__main__.py:139 +msgid "No files in this directory are expected for submission." +msgstr "Không có tập tin nào trong thư mục này để nộp." + +#: submit50/__main__.py:143 +msgid "Files that won't be submitted:" +msgstr "Các tập tin sẽ không nộp:" + +#: submit50/__main__.py:156 +msgid "" +"Keeping in mind the course's policy on academic honesty, including its " +"restrictions on AI use, are you sure you want to submit these files " +"(yes/no)? " +msgstr "" +"Cân nhắc quy định về tính trung thực trong học thuật, bao gồm cả " +"các hạn chế về việc sử dụng AI, bạn có chắc chắn muốn nộp các tập tin này không?" +"(có/không)? " + +#: submit50/__main__.py:174 submit50/__main__.py:194 +msgid "y|yes" +msgstr "c|có" + +#: submit50/__main__.py:189 +msgid "" +"You are submitting to a previous year's CS50x course. Your submission " +"will not be counted towards this year's course." +msgstr "" +"Bạn đang nộp bài cho khóa học CS50x của năm trước." +"Bài này sẽ không được tính vào khóa học năm nay." + +#: submit50/__main__.py:190 +msgid "" +"If you are looking to submit to this year's course, please use the " +"following slug:" +msgstr "" +"Nếu bạn muốn nộp bài cho khóa học năm nay, vui lòng sử dụng slug này: " + +#: submit50/__main__.py:194 +msgid "Do you want to continue with this submission (yes/no)? " +msgstr "Bạn có muốn tiếp tục nộp bài này không (có/không)?" + +#: submit50/__main__.py:195 +msgid "User aborted submission." +msgstr "Việc nộp bài bị hủy bỏ." + +#: submit50/__main__.py:207 +msgid "" +"Sorry, something's wrong, please try again. If the problem persists, " +"please visit our status page https://cs50.statuspage.io for more " +"information." +msgstr "" +"Rất tiếc, có gì xảy ra, vui lòng thử nộp lại. Nếu vấn đề này còn tiếp nữa, " +"vui lòng xem https://cs50.statuspage.io để biết thêm thông tin." + +#: submit50/__main__.py:212 +msgid "Submission cancelled." +msgstr "Việc nộp bài bị hủy bỏ" + +#: submit50/__main__.py:218 +msgid "logout of submit50" +msgstr "đăng xuất khỏi submit50" + +#: submit50/__main__.py:225 +msgid "failed to logout" +msgstr "không đăng xuất được" + +#: submit50/__main__.py:227 +msgid "logged out successfully" +msgstr "đăng xuất thành công" + +#: submit50/__main__.py:242 +msgid "" +"warning: displays usage warnings.\n" +"info: adds all commands run.\n" +"debug: adds the output of all commands run." +msgstr "" + +#: submit50/__main__.py:253 +msgid "prescribed identifier of work to submit" +msgstr "" + From b2ebb8c77e4af730917aad80e8768f9e4b20a019 Mon Sep 17 00:00:00 2001 From: ivanharvard <144486839+ivanharvard@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:08:33 -0400 Subject: [PATCH 02/11] add authentication flag and debug support --- submit50/__main__.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/submit50/__main__.py b/submit50/__main__.py index 0a3a72e..63eb148 100755 --- a/submit50/__main__.py +++ b/submit50/__main__.py @@ -23,6 +23,8 @@ # Internationalization gettext.install("submit50", str(files("submit50").joinpath("locale"))) +LOGGER = logging.getLogger("submit50") + SUBMIT_URL = "https://submit.cs50.io" class LogLevel(enum.IntEnum): @@ -193,7 +195,7 @@ def check_slug_year(slug): # Ask if they want to continue if not re.match(f"^\s*(?:{_('y|yes')})\s*$", input(_("Do you want to continue with this submission (yes/no)? ")), re.I): raise Error(_("User aborted submission.")) - + except ValueError: pass @@ -243,6 +245,12 @@ def main(): '\ninfo: adds all commands run.' '\ndebug: adds the output of all commands run.') ) + parser.add_argument("--https", + action="store_true", + help=_("force authentication via HTTPS")) + parser.add_argument("--ssh", + action="store_true", + help=_("force authentication via SSH")) parser.add_argument( "-V", "--version", action="version", @@ -260,8 +268,26 @@ def main(): check_announcements() check_version() check_slug_year(args.slug) - - user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt) + + # Decide whether to force HTTPS or SSH authentication + if args.https and args.ssh: + LOGGER.warning(_("--https and --ssh have no effect when used together")) + auth_method = None + elif args.https: + auth_method = "https" + elif args.ssh: + auth_method = "ssh" + else: + auth_method = None + + try: + user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt, auth_method=auth_method) + except lib50.ConnectionError: + LOGGER.debug(traceback.format_exc()) # log the traceback + raise Error(_( + "check50 failed to authenticate your Github account. Try running check50 again with --https or --ssh, " + "or try restarting your codespace. If the problem persists, please email us at sysadmins@cs50.harvard.edu." + )) print(message) if __name__ == "__main__": From e836ff17044e0676062e5b6dfb97c6dff5c30ae7 Mon Sep 17 00:00:00 2001 From: Rongxin Liu Date: Thu, 7 Aug 2025 00:02:48 -0400 Subject: [PATCH 03/11] improve error handling for GitHub authentication failures in codespaces --- submit50/__main__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/submit50/__main__.py b/submit50/__main__.py index 63eb148..b000684 100755 --- a/submit50/__main__.py +++ b/submit50/__main__.py @@ -195,7 +195,7 @@ def check_slug_year(slug): # Ask if they want to continue if not re.match(f"^\s*(?:{_('y|yes')})\s*$", input(_("Do you want to continue with this submission (yes/no)? ")), re.I): raise Error(_("User aborted submission.")) - + except ValueError: pass @@ -283,11 +283,15 @@ def main(): try: user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt, auth_method=auth_method) except lib50.ConnectionError: - LOGGER.debug(traceback.format_exc()) # log the traceback - raise Error(_( - "check50 failed to authenticate your Github account. Try running check50 again with --https or --ssh, " - "or try restarting your codespace. If the problem persists, please email us at sysadmins@cs50.harvard.edu." - )) + LOGGER.debug(traceback.format_exc()) + if not os.environ.get("CODESPACES"): + raise Error(_( + "submit50 failed to authenticate your Github account. Please make sure you are connected to the internet and try again." + )) + except Exception as e: + LOGGER.debug(traceback.format_exc()) + raise Error(_("Sorry, something's wrong, please try again.\n" + "If the problem persists, please visit our status page https://cs50.statuspage.io for more information.")) from e print(message) if __name__ == "__main__": From 6ee8c4bfbdea03683ce4923c7b6655b186754f0f Mon Sep 17 00:00:00 2001 From: Rongxin Liu Date: Thu, 7 Aug 2025 00:08:19 -0400 Subject: [PATCH 04/11] bump version to 3.2.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 57eaec9..35bbcd8 100755 --- a/setup.py +++ b/setup.py @@ -26,6 +26,6 @@ entry_points={ "console_scripts": ["submit50=submit50.__main__:main"] }, - version="3.2.1", + version="3.2.2", include_package_data=True ) From f4ee8f0e4513ed425e3c97f53afb37afe6793ba7 Mon Sep 17 00:00:00 2001 From: Brandon Nguyen <112731698+bxngyn@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:02:49 -0400 Subject: [PATCH 05/11] add compiled file --- submit50/locale/vi/LC_MESSAGES/submit50.mo | Bin 0 -> 2651 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 submit50/locale/vi/LC_MESSAGES/submit50.mo diff --git a/submit50/locale/vi/LC_MESSAGES/submit50.mo b/submit50/locale/vi/LC_MESSAGES/submit50.mo new file mode 100644 index 0000000000000000000000000000000000000000..f6b45096fed5ba24f3a468c9bf821fffb2309035 GIT binary patch literal 2651 zcmb_dU29xL7#_8L?UfWnkyc&>X$v_go3ydv^fOPyu%XKLkDryav1i-(LaW!+PISj70AB(= z0KNibPcwEN@G9^(;6>n3VBs0Yz5{*)bavyHEDwy5J6xfxv(IHoG7VI|h8(x<^X4;3zzPax9~Qjc{p z5;dxe*hphd-}HYH%A!+YCWDZxP(+a^`RuF_G4Y~~T@`~BoCjHsw2U8UDs3@GnxY{! zJ=WBoc18j9U}2rnS*U-0$o*fVi}-< z4Lrha7z$&`Nfb5RzT4B~Zh~WZ*1QOHv-w!Ri=KG1CGVVLQmdeELI?d(eZsS7 zGZk6#CX0 zAuoR@pNFBh07hJ(N87!g*%i34-crn!i8%BV(5bV90-c$do1vpSLLbi+xT+)=`T%!C zf8i|&UUNHiv`{!1Nngj6)G~C1P*` z7MlT0@}h_c1Rq8ScDmaB5t>=9lT!KAlS>MeQGO zY87+v4>utkwJuXproL^p{Y?#GO2a{?KAY@pURM;h?@}#6U`(#Ftds4uZiOzFYjrBN z*Cna;dXtv3llx&moIM{n!KT{RL)dR_;1ovRbT)s^4hD~W)%B6O4%it)OQ@J;(am5W zXjs)Q=3P$JPU{oovvs*fisQCu^y@dB)&`Z3F1T>lW149|h|mLCB5|$SVUI6Jnz^mszeBaG zZLU{Y Date: Thu, 7 Aug 2025 11:04:58 -0400 Subject: [PATCH 06/11] simplified error handling for GitHub authentication --- submit50/__main__.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/submit50/__main__.py b/submit50/__main__.py index b000684..08bc52d 100755 --- a/submit50/__main__.py +++ b/submit50/__main__.py @@ -280,18 +280,7 @@ def main(): else: auth_method = None - try: - user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt, auth_method=auth_method) - except lib50.ConnectionError: - LOGGER.debug(traceback.format_exc()) - if not os.environ.get("CODESPACES"): - raise Error(_( - "submit50 failed to authenticate your Github account. Please make sure you are connected to the internet and try again." - )) - except Exception as e: - LOGGER.debug(traceback.format_exc()) - raise Error(_("Sorry, something's wrong, please try again.\n" - "If the problem persists, please visit our status page https://cs50.statuspage.io for more information.")) from e + user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt, auth_method=auth_method) print(message) if __name__ == "__main__": From 9781b4745a5d93bb5e43b4827d991daf3b7cce82 Mon Sep 17 00:00:00 2001 From: Rongxin Liu Date: Thu, 22 Jan 2026 07:22:14 +0000 Subject: [PATCH 07/11] update workflow to set output for version using new syntax --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ff52931..e61a384 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: - name: Extract program version id: program_version run: | - echo ::set-output name=version::$(submit50 --version | cut --delimiter ' ' --fields 2) + echo "version=$(submit50 --version | cut --delimiter ' ' --fields 2)" >> $GITHUB_OUTPUT - name: Create Release if: ${{ github.ref == 'refs/heads/main' }} From 215b8702c47a2b9d65b3d0e8b794fedda4c5287f Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:24:13 -0400 Subject: [PATCH 08/11] update GitHub Actions workflow to use latest versions of checkout, setup-python, and github-script --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e61a384..eaadee3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,8 +3,8 @@ jobs: test-and-deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: "3.14" - name: Run tests @@ -31,7 +31,7 @@ jobs: - name: Create Release if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{ github.token }} script: | From 24a67d3b762adaf5f51d2b04077426fb7f7a0755 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:53:38 -0400 Subject: [PATCH 09/11] require lib50>=3.1.2 and improve forced-auth error handling lib50.push() only accepts auth_method from 3.1.2 onward, but the pin admitted 3.0.x-3.1.1, which made every submission fail with a TypeError for users whose lib50 pip had no reason to upgrade. Also: raise an actionable error when a forced --ssh login fails (lib50 raises a bare ConnectionError there), skip the localized yes-regex under RSTUDIO instead of matching a hard-coded "yes" against it, surface the both-flags warning via cprint like other warnings, and fix the missing space in the submit.cs50.io error text. --- setup.py | 5 +++-- submit50/__main__.py | 51 +++++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 35bbcd8..fee582f 100755 --- a/setup.py +++ b/setup.py @@ -16,10 +16,11 @@ }, description="This is submit50, with which you can submit solutions to problems for CS50.", long_description="This is submit50, with which you can submit solutions to problems for CS50.", - install_requires=["lib50>=3,<4", "packaging", "pytz", "requests>=2.19", "setuptools", "termcolor>=1.1"], + # lib50 >= 3.1.2 is the first release whose push() accepts auth_method + install_requires=["lib50>=3.1.2,<4", "packaging", "pytz", "requests>=2.19", "setuptools", "termcolor>=1.1"], keywords=["submit", "submit50"], name="submit50", - python_requires=">=3.6", + python_requires=">=3.8", license="GPLv3", packages=["submit50"], url="https://github.com/cs50/submit50", diff --git a/submit50/__main__.py b/submit50/__main__.py index 08bc52d..16da9c2 100755 --- a/submit50/__main__.py +++ b/submit50/__main__.py @@ -23,8 +23,6 @@ # Internationalization gettext.install("submit50", str(files("submit50").joinpath("locale"))) -LOGGER = logging.getLogger("submit50") - SUBMIT_URL = "https://submit.cs50.io" class LogLevel(enum.IntEnum): @@ -70,7 +68,7 @@ def check_version(package_name=__package__, timeout=5): # Retrieve version info res = requests.get(f"{SUBMIT_URL}/versions/submit50", timeout=timeout) if res.status_code != 200: - raise Error(_("Could not connect to submit.cs50.io." + raise Error(_("Could not connect to submit.cs50.io. " "Please visit our status page https://cs50.statuspage.io for more information.")) # Get the minimum required version from submit.cs50.io @@ -163,17 +161,17 @@ def prompt(honesty, included, excluded): honesty_question = str(honesty) # Get the user's answer - # If in R Studio environment, answer is always yes + # If in R Studio environment, the answer is always yes (skip the localized regex) if os.getenv("RSTUDIO") == "1": - answer = "yes" - else: - answer = input(honesty_question) + return True + + answer = input(honesty_question) except EOFError: answer = None print() # If no answer given, or yes is not given, don't continue - if not answer or not re.match(f"^\s*(?:{_('y|yes')})\s*$", answer, re.I): + if not answer or not re.match(rf"^\s*(?:{_('y|yes')})\s*$", answer, re.I): return False # Otherwise, do continue @@ -193,7 +191,7 @@ def check_slug_year(slug): cprint(suggested_slug, "yellow") # Ask if they want to continue - if not re.match(f"^\s*(?:{_('y|yes')})\s*$", input(_("Do you want to continue with this submission (yes/no)? ")), re.I): + if not re.match(rf"^\s*(?:{_('y|yes')})\s*$", input(_("Do you want to continue with this submission (yes/no)? ")), re.I): raise Error(_("User aborted submission.")) except ValueError: @@ -270,18 +268,33 @@ def main(): check_slug_year(args.slug) # Decide whether to force HTTPS or SSH authentication - if args.https and args.ssh: - LOGGER.warning(_("--https and --ssh have no effect when used together")) - auth_method = None - elif args.https: - auth_method = "https" - elif args.ssh: - auth_method = "ssh" - else: - auth_method = None + auth_method = resolve_auth_method(args.https, args.ssh) - user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt, auth_method=auth_method) + try: + user_name, commit_hash, message = lib50.push("submit50", args.slug, CONFIG_LOADER, prompt=prompt, auth_method=auth_method) + except lib50.ConnectionError as e: + # lib50 raises a bare ConnectionError when a forced SSH login fails (no HTTPS fallback); + # give the user something more actionable than the generic status-page message + if auth_method == "ssh" and not str(e): + raise Error(_("SSH authentication failed. Make sure your SSH key is added to your GitHub account " + "and loaded in ssh-agent, or omit --ssh to authenticate via HTTPS instead.")) + raise print(message) + +def resolve_auth_method(https, ssh): + """ + Map the --https/--ssh flags to lib50's auth_method ("https", "ssh", or None for lib50's default). + Warn and fall back to the default when both flags are given. + """ + if https and ssh: + cprint(_("--https and --ssh have no effect when used together"), "yellow") + return None + if https: + return "https" + if ssh: + return "ssh" + return None + if __name__ == "__main__": main() From f056333e6a480b1e97ea6ab16f3c91fd3b8b1b3d Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:53:38 -0400 Subject: [PATCH 10/11] complete vietnamese catalog and fix concatenated msgstr spacing Translate the --https/--ssh help, --log-level help, slug help, the combined-flag warning and the new SSH failure message; restore the missing spaces where multi-line msgstr fragments were joined; accept ASCII "co" as an affirmative; distinguish "User aborted submission." from "Submission cancelled."; recompile the committed .mo. --- submit50/locale/vi/LC_MESSAGES/submit50.mo | Bin 2651 -> 3725 bytes submit50/locale/vi/LC_MESSAGES/submit50.po | 60 ++++++++++++++------- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/submit50/locale/vi/LC_MESSAGES/submit50.mo b/submit50/locale/vi/LC_MESSAGES/submit50.mo index f6b45096fed5ba24f3a468c9bf821fffb2309035..fa1a2cf1d8175f3a3e165a0b680b2aac7c576260 100644 GIT binary patch delta 1555 zcma))U1%It6vyw{&6=#XNNct9Lyl-$AG%9ys?m-3K^}~XqD_%LNO3-Q#@)SlJ98&- zZDffRg9Jb7AR0y5u#|vcb%`MEK%`6%9}1!mz7&Lc@ImlJeDbOO@60sDYQce--`sQW zIp=@RIhSvJYhU;0?Zx*Qp4;&5#d}FDHa&9%#>TLI74*P9I1b)@17rKZ7r;BfSHPp- z5~u?H2C8$fZDH(VZ~(psK3`;PC-^J4i?J@df}a3}T|pvk8aAU^Tf??#`;BY9()5-6&T){2|WO+1d};WfNH-3*1!+IKKLUDE$sDe z_>5I5j+AYmG;UI*;`SKjp3f<`` z(gGN{bPY|qf|ISeP0Os((^|tKKkzK-h`^(1oNQPfa*@h3(`ebow*PZcI?;ThJo z5;xAW>uy7C*eV|Cwmr)?JXcq8OqI-bEeNq`R(K6m1UJ;7ip^>scX_==rt7zxTE|Dp zAe?f$UoE@b7BzKu6rkB8L$q3`xldlet7X&D130IOTlnPgBi-ZivO&^G zT~&+mJ2)>%PX^0wm8RlehYXlLH!^VYl0hF~k$zQ6hr&pr6)kn(w0{d*KuUr+P2JBw&E8iiv$#M9$t zK{H7-pIROAljt)tljuCJ=W{xn7hk5`RBl-S%*aA22=!5YUG8kmZq}_vGWbT)$sqMe QJNrLEFk{XTa~MVT7b-asKL7v# delta 481 zcmajaK}!Nr5C`yiwyv%zgd|0|u|?7$jFmFA0wFIQ3?e8g$b%B(Me5L@Ff529B7}wr zLnyj*tH4XN5Qv95)dvuQJxHexeSzq|bg5GV!*Av>%zN)xZFUylywWKlI*@*3)oOl) zUl47eS78K}p$ea1A8f&SDA|atMJZ^_eZUhKXe08$d)N(M;3j;BxU)zr5>Xcp^3V$p zpdD6V3Cl0wHu@o%Xc}I@9@v8YkQ_uyU;w_t0-SxGK%o25o1emXc-!H|@h*k%vTx>++;09Mq#hJ*\n" "Language: vi\n" "Language-Team: vi \n" @@ -16,11 +16,11 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: submit50/__main__.py:71 msgid "" -"Could not connect to submit.cs50.io.Please visit our status page " +"Could not connect to submit.cs50.io. Please visit our status page " "https://cs50.statuspage.io for more information." msgstr "" "Không kết nối đến submit.cs50.io được. Vui lòng xem " @@ -44,36 +44,35 @@ msgid "" "restrictions on AI use, are you sure you want to submit these files " "(yes/no)? " msgstr "" -"Cân nhắc quy định về tính trung thực trong học thuật, bao gồm cả " -"các hạn chế về việc sử dụng AI, bạn có chắc chắn muốn nộp các tập tin này không?" +"Cân nhắc quy định về tính trung thực trong học thuật, bao gồm cả các hạn " +"chế về việc sử dụng AI, bạn có chắc chắn muốn nộp các tập tin này không " "(có/không)? " #: submit50/__main__.py:174 submit50/__main__.py:194 msgid "y|yes" -msgstr "c|có" +msgstr "c|co|có" #: submit50/__main__.py:189 msgid "" "You are submitting to a previous year's CS50x course. Your submission " "will not be counted towards this year's course." msgstr "" -"Bạn đang nộp bài cho khóa học CS50x của năm trước." -"Bài này sẽ không được tính vào khóa học năm nay." +"Bạn đang nộp bài cho khóa học CS50x của năm trước. Bài này sẽ không được " +"tính vào khóa học năm nay." #: submit50/__main__.py:190 msgid "" "If you are looking to submit to this year's course, please use the " "following slug:" -msgstr "" -"Nếu bạn muốn nộp bài cho khóa học năm nay, vui lòng sử dụng slug này: " +msgstr "Nếu bạn muốn nộp bài cho khóa học năm nay, vui lòng sử dụng slug này:" #: submit50/__main__.py:194 msgid "Do you want to continue with this submission (yes/no)? " -msgstr "Bạn có muốn tiếp tục nộp bài này không (có/không)?" +msgstr "Bạn có muốn tiếp tục nộp bài này không (có/không)? " #: submit50/__main__.py:195 msgid "User aborted submission." -msgstr "Việc nộp bài bị hủy bỏ." +msgstr "Người dùng đã hủy nộp bài." #: submit50/__main__.py:207 msgid "" @@ -81,12 +80,12 @@ msgid "" "please visit our status page https://cs50.statuspage.io for more " "information." msgstr "" -"Rất tiếc, có gì xảy ra, vui lòng thử nộp lại. Nếu vấn đề này còn tiếp nữa, " -"vui lòng xem https://cs50.statuspage.io để biết thêm thông tin." +"Rất tiếc, có gì xảy ra, vui lòng thử nộp lại. Nếu vấn đề này còn tiếp " +"nữa, vui lòng xem https://cs50.statuspage.io để biết thêm thông tin." #: submit50/__main__.py:212 msgid "Submission cancelled." -msgstr "Việc nộp bài bị hủy bỏ" +msgstr "Đã hủy nộp bài." #: submit50/__main__.py:218 msgid "logout of submit50" @@ -106,8 +105,33 @@ msgid "" "info: adds all commands run.\n" "debug: adds the output of all commands run." msgstr "" +"warning: hiển thị các cảnh báo về cách sử dụng.\n" +"info: thêm tất cả các lệnh đã chạy.\n" +"debug: thêm kết quả của tất cả các lệnh đã chạy." + +#: submit50/__main__.py:248 +msgid "force authentication via HTTPS" +msgstr "buộc xác thực qua HTTPS" + +#: submit50/__main__.py:251 +msgid "force authentication via SSH" +msgstr "buộc xác thực qua SSH" -#: submit50/__main__.py:253 +#: submit50/__main__.py:259 msgid "prescribed identifier of work to submit" +msgstr "định danh được chỉ định của bài cần nộp" + +#: submit50/__main__.py:279 +msgid "" +"SSH authentication failed. Make sure your SSH key is added to your GitHub" +" account and loaded in ssh-agent, or omit --ssh to authenticate via HTTPS" +" instead." msgstr "" +"Xác thực SSH thất bại. Hãy chắc chắn rằng khóa SSH của bạn đã được thêm " +"vào tài khoản GitHub và đã được nạp vào ssh-agent, hoặc bỏ --ssh để xác " +"thực qua HTTPS." + +#: submit50/__main__.py:291 +msgid "--https and --ssh have no effect when used together" +msgstr "--https và --ssh không có hiệu lực khi dùng cùng nhau" From 1327b9c7056ad599ab82273362f01d9724971c52 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:53:38 -0400 Subject: [PATCH 11/11] add smoke tests and run them against the minimum lib50 in CI CI previously only ran `submit50 --help`, so a lib50 API mismatch could ship to PyPI unnoticed. The new tests wire each --https/--ssh flag combination through main() to lib50.push, check the forced-SSH error path, and verify every _() string is translated and the committed .mo matches its .po. The workflow runs them against both the newest lib50 and the floor declared in setup.py. --- .github/workflows/main.yml | 11 +++- tests/test_locale.py | 91 +++++++++++++++++++++++++++++ tests/test_main.py | 114 +++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 tests/test_locale.py create mode 100644 tests/test_main.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eaadee3..3c97723 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,10 +9,19 @@ jobs: python-version: "3.14" - name: Run tests run: | - pip install babel + pip install babel pytest pip install . submit50 --help python setup.py compile_catalog + pytest -q + - name: Run tests against the minimum supported lib50 + run: | + # Exercise the oldest lib50 that setup.py admits, so an API drift (e.g. a missing + # push() kwarg) fails here instead of on students' machines. + floor=$(python -c "import re; print(re.search(r'lib50>=([\d.]+)', open('setup.py').read()).group(1))") + pip install "lib50==$floor" + pytest -q + pip install --upgrade "lib50<4" - name: Install pypa/build run: python -m pip install build --user - name: Build a binary wheel and a source tarball diff --git a/tests/test_locale.py b/tests/test_locale.py new file mode 100644 index 0000000..e3292ad --- /dev/null +++ b/tests/test_locale.py @@ -0,0 +1,91 @@ +"""Consistency checks for the gettext catalogs under submit50/locale.""" +import ast +import gettext +import io +import pathlib +import re + +import pytest +from babel.messages.mofile import write_mo +from babel.messages.pofile import read_po + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SOURCE = ROOT / "submit50" / "__main__.py" +LOCALE_DIR = ROOT / "submit50" / "locale" +LOCALES = sorted(p.name for p in LOCALE_DIR.iterdir() if (p / "LC_MESSAGES" / "submit50.po").is_file()) + +# Catalogs known to be incomplete before the completeness check existed. +KNOWN_INCOMPLETE = {"es"} + + +def source_strings(): + """Every string literal passed to _() in __main__.py.""" + literals = set() + for node in ast.walk(ast.parse(SOURCE.read_text())): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_" + and node.args + and isinstance(node.args[0], ast.Constant) + and isinstance(node.args[0].value, str) + ): + literals.add(node.args[0].value) + assert literals, "no _() literals found -- extraction is broken" + return literals + + +def load_po(locale): + with open(LOCALE_DIR / locale / "LC_MESSAGES" / "submit50.po", "rb") as f: + return read_po(f, locale=locale) + + +@pytest.mark.parametrize("locale", LOCALES) +def test_every_source_string_is_translated(locale, request): + if locale in KNOWN_INCOMPLETE: + request.applymarker(pytest.mark.xfail(reason=f"{locale} catalog is known to be incomplete", strict=True)) + catalog = load_po(locale) + translations = {m.id: m.string for m in catalog if m.id} + missing = sorted(s for s in source_strings() if s not in translations) + empty = sorted(s for s in source_strings() if s in translations and not translations[s]) + fuzzy = sorted(m.id for m in catalog if m.id and m.fuzzy) + assert not missing, f"{locale}: strings missing from catalog: {missing}" + assert not empty, f"{locale}: untranslated strings: {empty}" + assert not fuzzy, f"{locale}: fuzzy entries are skipped by compile_catalog: {fuzzy}" + + +@pytest.mark.parametrize("locale", LOCALES) +def test_prompt_translations_keep_trailing_space(locale): + """input() prompts end with a space in the source; a translation that drops it glues the cursor to the text.""" + for message in load_po(locale): + if message.id and message.string and message.id.endswith(" "): + assert message.string.endswith(" "), f"{locale}: translation of {message.id!r} lost its trailing space" + + +@pytest.mark.parametrize("locale", LOCALES) +def test_yes_regex_translation_is_valid(locale): + """`y|yes` is interpolated into a regex; the translation must compile and accept its own affirmative.""" + catalog = load_po(locale) + translated = catalog.get("y|yes") + if translated is None or not translated.string: + pytest.skip(f"{locale}: y|yes not translated") + pattern = re.compile(rf"^\s*(?:{translated.string})\s*$", re.I) + first_alternative = translated.string.split("|")[0] + assert pattern.match(first_alternative), f"{locale}: regex rejects its own first alternative" + assert not pattern.match("no"), f"{locale}: regex accepts 'no'" + + +@pytest.mark.parametrize("locale", LOCALES) +def test_committed_mo_matches_po(locale): + """A committed .mo must be the compiled form of the committed .po (`*.mo` is gitignored, so it drifts silently).""" + mo_path = LOCALE_DIR / locale / "LC_MESSAGES" / "submit50.mo" + if not mo_path.is_file(): + pytest.skip(f"{locale}: no .mo committed (CI compiles it at build time)") + buf = io.BytesIO() + write_mo(buf, load_po(locale)) + expected = gettext.GNUTranslations(io.BytesIO(buf.getvalue()))._catalog + with open(mo_path, "rb") as f: + actual = gettext.GNUTranslations(f)._catalog + expected.pop("", None) + actual.pop("", None) + assert actual == expected, f"{locale}: submit50.mo is stale -- run `python setup.py compile_catalog` and recommit" diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..adf4e5c --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,114 @@ +"""Smoke tests for submit50's CLI wiring against lib50. + +These run in CI against both the newest lib50 and the minimum version declared in +setup.py, so a lib50 API drift (e.g. a missing ``auth_method`` kwarg) fails the +build instead of every student's submission. +""" +import inspect +import re +import subprocess +import sys + +import lib50 +import pytest + +import submit50.__main__ as cli + + +def test_lib50_push_accepts_auth_method(): + assert "auth_method" in inspect.signature(lib50.push).parameters + + +@pytest.mark.parametrize( + "https, ssh, expected", + [ + (False, False, None), + (True, False, "https"), + (False, True, "ssh"), + (True, True, None), + ], +) +def test_resolve_auth_method(https, ssh, expected, capsys): + assert cli.resolve_auth_method(https, ssh) == expected + out = capsys.readouterr().out + if https and ssh: + assert "--https and --ssh" in out + else: + assert out == "" + + +def _stub_checks(monkeypatch): + """Skip the network-dependent preflight checks.""" + monkeypatch.setattr(cli, "check_announcements", lambda: None) + monkeypatch.setattr(cli, "check_version", lambda: None) + monkeypatch.setattr(cli, "check_slug_year", lambda slug: None) + + +@pytest.mark.parametrize( + "flags, expected", + [ + ([], None), + (["--https"], "https"), + (["--ssh"], "ssh"), + (["--https", "--ssh"], None), + ], +) +def test_main_passes_auth_method_to_lib50(flags, expected, monkeypatch, capsys): + _stub_checks(monkeypatch) + calls = [] + + def fake_push(tool, slug, config_loader, **kwargs): + calls.append((tool, slug, kwargs)) + return "user", "deadbeef", "pushed" + + monkeypatch.setattr(lib50, "push", fake_push) + monkeypatch.setattr(sys, "argv", ["submit50", *flags, "cs50/problems/2026/x/hello"]) + + cli.main() + + assert len(calls) == 1 + tool, slug, kwargs = calls[0] + assert (tool, slug) == ("submit50", "cs50/problems/2026/x/hello") + assert kwargs["auth_method"] == expected + assert kwargs["prompt"] is cli.prompt + assert "pushed" in capsys.readouterr().out + + +def test_forced_ssh_failure_gets_actionable_error(monkeypatch): + _stub_checks(monkeypatch) + + def fail_push(*args, **kwargs): + raise lib50.ConnectionError # lib50 raises this bare when a forced SSH login fails + + monkeypatch.setattr(lib50, "push", fail_push) + monkeypatch.setattr(sys, "argv", ["submit50", "--ssh", "cs50/problems/2026/x/hello"]) + + with pytest.raises(cli.Error, match="SSH authentication failed"): + cli.main() + + +def test_unforced_connection_error_is_not_rewritten(monkeypatch): + _stub_checks(monkeypatch) + + def fail_push(*args, **kwargs): + raise lib50.ConnectionError + + monkeypatch.setattr(lib50, "push", fail_push) + monkeypatch.setattr(sys, "argv", ["submit50", "cs50/problems/2026/x/hello"]) + + with pytest.raises(lib50.ConnectionError): + cli.main() + + +def test_rstudio_skips_honesty_prompt(monkeypatch): + monkeypatch.setenv("RSTUDIO", "1") + monkeypatch.setattr("builtins.input", lambda *a: pytest.fail("input() must not be called under RSTUDIO")) + assert cli.prompt(True, ["hello.c"], []) is True + + +def test_version_output_shape(): + """The release workflow extracts the tag with `cut -d' ' -f2`; keep ` `.""" + out = subprocess.run( + [sys.executable, "-m", "submit50", "--version"], capture_output=True, text=True, check=True + ).stdout.strip() + assert re.fullmatch(r"submit50 \d+\.\d+\.\d+", out), out