From 89af3ef1f0c75e4e4ae3a8f508e10bb707682e6a Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Thu, 1 Aug 2019 17:22:12 -0400 Subject: [PATCH 01/10] check50 remote --- check50/__main__.py | 61 ++++++++++++++++++---------------- check50/renderer/_renderers.py | 7 ++-- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/check50/__main__.py b/check50/__main__.py index 8ba939fd..d0c01044 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -157,34 +157,32 @@ def compile_checks(checks, prompt=False): -def await_results(url, pings=45, sleep=2): +def await_results(url, params={}, pings=45, sleep=2): """ Ping {url} until it returns a results payload, timing out after {pings} pings and waiting {sleep} seconds between pings. """ - print("Checking...", end="", flush=True) - for _ in range(pings): + for _i in range(pings): # Query for check results. - res = requests.post(url, params={"format": "json"}) - if res.status_code == 200: - print() + res = requests.get(url, params=params) + payload = res.json() + if res.status_code == 200 and len(payload) and payload[0]["check50"] != None and payload[0]["check50"]: + results = payload[0] break - print(".", end="", flush=True) time.sleep(sleep) else: # Terminate if no response - print() raise internal.Error( - _("check50 is taking longer than normal!\nSee {} for more detail.").format(url)) + _("Timed out waiting for check results! Please email sysadmins@cs50.harvard.edu.")) + - payload= res.json() # TODO: Should probably check payload["version"] here to make sure major version is same as __version__ # (otherwise we may not be able to parse results) - return { - "slug": payload["slug"], - "results": list(map(CheckResult.from_dict, payload["results"])), - "version": payload["version"] + return results["tag_hash"], { + "slug": results["check50"]["slug"], + "results": list(map(CheckResult.from_dict, results["check50"]["results"])), + "version": results["check50"]["version"] } @@ -233,9 +231,10 @@ def main(): parser.add_argument("--offline", action="store_true", help=_("run checks completely offline (implies --local)")) + parser.add_argument("--remote", action="store_true", help=_("run checks remotely")) parser.add_argument("-l", "--local", action="store_true", - help=_("run checks locally instead of uploading to cs50 (enabled by default in beta version)")) + help=_("run checks locally instead of uploading to cs50 (enabled by default in beta version)"), default=True) parser.add_argument("--log", action="store_true", help=_("display more detailed information about check results")) @@ -266,8 +265,8 @@ def main(): global SLUG SLUG = args.slug - # TODO: remove this when submit.cs50.io API is stabilized - args.local = True + if args.remote: + args.local = args.offline = args.dev = False if args.dev: args.offline = True @@ -291,7 +290,11 @@ def main(): excepthook.outputs = args.output excepthook.output_file = args.output_file - if args.local: + if args.remote: + commit_hash = lib50.push("check50", SLUG, internal.CONFIG_LOADER, commit_suffix="[check50=true]")[1] + with lib50.ProgressBar("Waiting for results") if "ansi" in args.output else nullcontext(): + tag_hash, results = await_results("https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash, "slug": SLUG}) + else: with lib50.ProgressBar("Checking") if not args.verbose and "ansi" in args.output else nullcontext(): # If developing, assume slug is a path to check_dir if args.dev: @@ -351,11 +354,6 @@ def main(): "version": __version__ } - else: - # TODO: Remove this before we ship - raise NotImplementedError("cannot run check50 remotely, until version 3.0.0 is shipped ") - commit_hash = lib50.push("check50", SLUG, commit_suffix="[submit=false]")[1] - results = await_results(f"https://check.cs50.io/{commit_hash}") # Render output file_manager = open(args.output_file, "w") if args.output_file else nullcontext(sys.stdout) @@ -368,13 +366,18 @@ def main(): output_file.write(renderer.to_ansi(**results, log=args.log)) output_file.write("\n") elif output == "html": - html = renderer.to_html(**results) - if os.environ.get("CS50_IDE_TYPE"): - subprocess.check_call(["c9", "exec", "rendercheckresults", html]) + if args.remote: + url = f"https://submit.cs50.io/check50/{tag_hash}" else: - with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html") as html_file: - html_file.write(html) - termcolor.cprint(_("To see the results in your browser go to file://{}").format(html_file.name), "white", attrs=["bold"]) + html = renderer.to_html(**results) + if os.environ.get("CS50_IDE_TYPE"): + subprocess.check_call(["c9", "exec", "rendercheckresults", html]) + else: + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html") as html_file: + html_file.write(html) + url = f"file://{html_file.name}" + termcolor.cprint(_("To see the results in your browser go to {}").format(url), "white", attrs=["bold"]) + if __name__ == "__main__": diff --git a/check50/renderer/_renderers.py b/check50/renderer/_renderers.py index 7013da48..ee36e0fc 100644 --- a/check50/renderer/_renderers.py +++ b/check50/renderer/_renderers.py @@ -7,7 +7,6 @@ from pexpect.exceptions import EOF import termcolor -from .. import __version__ from ..runner import CheckResult TEMPLATES = pathlib.Path(pkg_resources.resource_filename("check50.renderer", "templates")) @@ -25,7 +24,7 @@ def default(self, o): return o.__dict__ -def to_html(slug, results, version=__version__): +def to_html(slug, results, version): with open(TEMPLATES / "results.html") as f: content = f.read() @@ -36,13 +35,13 @@ def to_html(slug, results, version=__version__): return html -def to_json(slug, results, version=__version__): +def to_json(slug, results, version): return json.dumps({"slug": slug, "results": results, "version": version}, cls=Encoder, indent=4) -def to_ansi(slug, results, version=__version__, log=False): +def to_ansi(slug, results, version, log=False): lines = [termcolor.colored(_("Results for {} generated by check50 v{}").format(slug, version), "white", attrs=["bold"])] for result in results: if result.passed: From 2d8b063ed228efe181e4c9c350fc49820a4b9189 Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Mon, 5 Aug 2019 16:24:33 -0400 Subject: [PATCH 02/10] add check50_loglevel --- check50/__main__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/check50/__main__.py b/check50/__main__.py index d0c01044..c14ec64c 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -176,7 +176,6 @@ def await_results(url, params={}, pings=45, sleep=2): raise internal.Error( _("Timed out waiting for check results! Please email sysadmins@cs50.harvard.edu.")) - # TODO: Should probably check payload["version"] here to make sure major version is same as __version__ # (otherwise we may not be able to parse results) return results["tag_hash"], { @@ -277,7 +276,7 @@ def main(): if args.verbose: # Show lib50 commands being run in verbose mode - logging.basicConfig(level="INFO") + logging.basicConfig(level=os.environ.get("CHECK50_LOGLEVEL", "INFO")) lib50.ProgressBar.DISABLED = True args.log = True From 99356494688d253e42df654448c9b70417952677 Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Mon, 5 Aug 2019 16:24:44 -0400 Subject: [PATCH 03/10] version++ --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 36c03f19..24d73ca9 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,6 @@ "console_scripts": ["check50=check50.__main__:main"] }, url="https://github.com/cs50/check50", - version="3.0.4", + version="3.0.5", include_package_data=True ) From 8d0016037cbf2c4bff05ea3a817d85d587c709fe Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Mon, 5 Aug 2019 16:28:04 -0400 Subject: [PATCH 04/10] increase number of pings --- check50/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check50/__main__.py b/check50/__main__.py index c14ec64c..8ff30b41 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -157,7 +157,7 @@ def compile_checks(checks, prompt=False): -def await_results(url, params={}, pings=45, sleep=2): +def await_results(url, params={}, pings=90, sleep=2): """ Ping {url} until it returns a results payload, timing out after {pings} pings and waiting {sleep} seconds between pings. From 669a459a2bf8757b81871eb5f5aee42651e344dc Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Mon, 5 Aug 2019 16:33:02 -0400 Subject: [PATCH 05/10] remove 'in beta version' --- check50/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check50/__main__.py b/check50/__main__.py index 8ff30b41..69a98aa0 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -233,7 +233,7 @@ def main(): parser.add_argument("--remote", action="store_true", help=_("run checks remotely")) parser.add_argument("-l", "--local", action="store_true", - help=_("run checks locally instead of uploading to cs50 (enabled by default in beta version)"), default=True) + help=_("run checks locally instead of uploading to cs50 (enabled by default)"), default=True) parser.add_argument("--log", action="store_true", help=_("display more detailed information about check results")) From 8c654a1605514c93032cbf67a2a687a8bea397c5 Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Tue, 6 Aug 2019 16:01:22 -0400 Subject: [PATCH 06/10] finalize(?) remote support, make default --- check50/__main__.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/check50/__main__.py b/check50/__main__.py index 69a98aa0..5465a32c 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -30,6 +30,12 @@ SLUG = None +class RemoteCheckError(internal.Error): + def __init__(self, remote_json): + super().__init__("check50 ran into an error while running checks! Please contact sysadmins@cs50.harvard.edu!") + self.payload = {"remote_json": remote_json} + + @contextlib.contextmanager def nullcontext(entry_result=None): """This is just contextlib.nullcontext but that function is only available in 3.7+.""" @@ -41,9 +47,8 @@ def excepthook(cls, exc, tb): outputs = excepthook.outputs for output in excepthook.outputs: + outputs.remove(output) if output == "json": - outputs.remove("json") - ctxmanager = open(excepthook.output_file, "w") if excepthook.output_file else nullcontext(sys.stdout) with ctxmanager as output_file: json.dump({ @@ -59,11 +64,6 @@ def excepthook(cls, exc, tb): output_file.write("\n") elif output == "ansi" or output == "html": - if output == "ansi": - outputs.remove("ansi") - else: - outputs.remove("html") - if (issubclass(cls, internal.Error) or issubclass(cls, lib50.Error)) and exc.args: termcolor.cprint(str(exc), "red", file=sys.stderr) elif issubclass(cls, FileNotFoundError): @@ -78,6 +78,8 @@ def excepthook(cls, exc, tb): if excepthook.verbose: traceback.print_exception(cls, exc, tb) + if hasattr(exc, "payload"): + print("Exception payload:", exc.payload) sys.exit(1) @@ -157,7 +159,7 @@ def compile_checks(checks, prompt=False): -def await_results(url, params={}, pings=90, sleep=2): +def await_results(commit_hash, slug, pings=45, sleep=2): """ Ping {url} until it returns a results payload, timing out after {pings} pings and waiting {sleep} seconds between pings. @@ -165,7 +167,7 @@ def await_results(url, params={}, pings=90, sleep=2): for _i in range(pings): # Query for check results. - res = requests.get(url, params=params) + res = requests.get(f"https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash, "slug": slug}) payload = res.json() if res.status_code == 200 and len(payload) and payload[0]["check50"] != None and payload[0]["check50"]: results = payload[0] @@ -174,7 +176,12 @@ def await_results(url, params={}, pings=90, sleep=2): else: # Terminate if no response raise internal.Error( - _("Timed out waiting for check results! Please email sysadmins@cs50.harvard.edu.")) + _("check50 is taking longer than normal!\n" + "See https://submit.cs50.io/check50/{} for more detail").format(commit_hash)) + + if "error" in results["check50"]: + raise RemoteCheckError(results["check50"]) + # TODO: Should probably check payload["version"] here to make sure major version is same as __version__ # (otherwise we may not be able to parse results) @@ -230,10 +237,10 @@ def main(): parser.add_argument("--offline", action="store_true", help=_("run checks completely offline (implies --local)")) - parser.add_argument("--remote", action="store_true", help=_("run checks remotely")) + parser.add_argument("--remote", action="store_true", help=_("run checks remotely (default)"), default=True) parser.add_argument("-l", "--local", action="store_true", - help=_("run checks locally instead of uploading to cs50 (enabled by default)"), default=True) + help=_("run checks locally instead of uploading to cs50")) parser.add_argument("--log", action="store_true", help=_("display more detailed information about check results")) @@ -292,7 +299,7 @@ def main(): if args.remote: commit_hash = lib50.push("check50", SLUG, internal.CONFIG_LOADER, commit_suffix="[check50=true]")[1] with lib50.ProgressBar("Waiting for results") if "ansi" in args.output else nullcontext(): - tag_hash, results = await_results("https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash, "slug": SLUG}) + tag_hash, results = await_results(commit_hash, SLUG) else: with lib50.ProgressBar("Checking") if not args.verbose and "ansi" in args.output else nullcontext(): # If developing, assume slug is a path to check_dir From 3728fdaaf1da66f51eb8554168a842aac936675f Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Wed, 7 Aug 2019 14:24:09 -0400 Subject: [PATCH 07/10] update check50 remote to reflect updated api --- check50/__main__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/check50/__main__.py b/check50/__main__.py index 5465a32c..763d42cc 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -167,10 +167,9 @@ def await_results(commit_hash, slug, pings=45, sleep=2): for _i in range(pings): # Query for check results. - res = requests.get(f"https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash, "slug": slug}) - payload = res.json() - if res.status_code == 200 and len(payload) and payload[0]["check50"] != None and payload[0]["check50"]: - results = payload[0] + res = requests.get(f"https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash}) + results = res.json() + if res.status_code == 200 and results["received_at"] is not None: break time.sleep(sleep) else: @@ -179,6 +178,10 @@ def await_results(commit_hash, slug, pings=45, sleep=2): _("check50 is taking longer than normal!\n" "See https://submit.cs50.io/check50/{} for more detail").format(commit_hash)) + + if not results["check50"]: + raise RemoteCheckError(results) + if "error" in results["check50"]: raise RemoteCheckError(results["check50"]) From 806a7d7fe1b5f5e10fd349ac2fc4b5ed594af6c4 Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Wed, 7 Aug 2019 14:46:16 -0400 Subject: [PATCH 08/10] stop waiting if status code isn't 404 or 200 --- check50/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/check50/__main__.py b/check50/__main__.py index 763d42cc..4aa17456 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -167,8 +167,12 @@ def await_results(commit_hash, slug, pings=45, sleep=2): for _i in range(pings): # Query for check results. - res = requests.get(f"https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash}) + res = requests.get(f"https://submit.cs50.io/api/results/check50?check50", params={"commit_hash": commit_hash, "slug": slug}) results = res.json() + + if res.status_code not in [404, 200]: + raise RemoteCheckError(results) + if res.status_code == 200 and results["received_at"] is not None: break time.sleep(sleep) From 9b0006be2af2ac984ba6ff7ff7d6c345c2038bd0 Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Wed, 7 Aug 2019 14:49:56 -0400 Subject: [PATCH 09/10] fix default logic --- check50/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/check50/__main__.py b/check50/__main__.py index 4aa17456..c1ef7c02 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -278,8 +278,6 @@ def main(): global SLUG SLUG = args.slug - if args.remote: - args.local = args.offline = args.dev = False if args.dev: args.offline = True @@ -294,6 +292,9 @@ def main(): lib50.ProgressBar.DISABLED = True args.log = True + if args.local: + args.remote = False + # Filter out any duplicates from args.output seen_output = set() args.output = [output for output in args.output if not (output in seen_output or seen_output.add(output))] From 52279f57c12dcde887e2b386a0386b87b44a8cac Mon Sep 17 00:00:00 2001 From: Chad Sharp Date: Wed, 7 Aug 2019 14:51:23 -0400 Subject: [PATCH 10/10] remove args.remote --- check50/__main__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/check50/__main__.py b/check50/__main__.py index c1ef7c02..637ba5aa 100644 --- a/check50/__main__.py +++ b/check50/__main__.py @@ -244,7 +244,6 @@ def main(): parser.add_argument("--offline", action="store_true", help=_("run checks completely offline (implies --local)")) - parser.add_argument("--remote", action="store_true", help=_("run checks remotely (default)"), default=True) parser.add_argument("-l", "--local", action="store_true", help=_("run checks locally instead of uploading to cs50")) @@ -292,9 +291,6 @@ def main(): lib50.ProgressBar.DISABLED = True args.log = True - if args.local: - args.remote = False - # Filter out any duplicates from args.output seen_output = set() args.output = [output for output in args.output if not (output in seen_output or seen_output.add(output))] @@ -304,7 +300,7 @@ def main(): excepthook.outputs = args.output excepthook.output_file = args.output_file - if args.remote: + if not args.local: commit_hash = lib50.push("check50", SLUG, internal.CONFIG_LOADER, commit_suffix="[check50=true]")[1] with lib50.ProgressBar("Waiting for results") if "ansi" in args.output else nullcontext(): tag_hash, results = await_results(commit_hash, SLUG) @@ -380,7 +376,7 @@ def main(): output_file.write(renderer.to_ansi(**results, log=args.log)) output_file.write("\n") elif output == "html": - if args.remote: + if not args.local: url = f"https://submit.cs50.io/check50/{tag_hash}" else: html = renderer.to_html(**results)