diff --git a/examples/interactive.py b/examples/interactive.py new file mode 100644 index 0000000..db7a906 --- /dev/null +++ b/examples/interactive.py @@ -0,0 +1,80 @@ +import judge0 + +user_solution = judge0.Submission( + source_code=""" +lower_bound, upper_bound = (int(x) for x in input().strip().split()) +while True: + my_guess = (lower_bound + upper_bound) // 2 + print(my_guess) + + command = input().strip() + if command == "lower": + lower_bound, upper_bound = lower_bound, my_guess + elif command == "higher": + lower_bound, upper_bound = my_guess, upper_bound + else: + break +""" +) + +interactive_producer = judge0.Submission( + source_code=""" +#include + +int main(int argc, char **argv) { + int lower_bound, upper_bound, number_to_guess, max_tries; + scanf("%d %d %d %d", &lower_bound, &upper_bound, &number_to_guess, &max_tries); + + FILE *user_solution_stdin = fopen(argv[1], "w"); + FILE *user_solution_stdout = fopen(argv[2], "r"); + + fprintf(user_solution_stdin, "%d %d\\n", lower_bound, upper_bound); + fflush(user_solution_stdin); + + int user_guess; + for (int i = 0; i < max_tries; i++) { + fscanf(user_solution_stdout, "%d", &user_guess); + if (user_guess > number_to_guess) { + fprintf(user_solution_stdin, "lower\\n"); + fflush(user_solution_stdin); + } else if (user_guess < number_to_guess) { + fprintf(user_solution_stdin, "higher\\n"); + fflush(user_solution_stdin); + } else { + fprintf(user_solution_stdin, "correct\\n"); + fflush(user_solution_stdin); + + printf("User successfully guessed the number.\\n"); + + return 0; + } + } + + fprintf(user_solution_stdin, "failed\\n"); + fflush(user_solution_stdin); + + printf("User failed to guess the number within %d guesses.\\n", max_tries); + + return 0; +} +""", + language=judge0.C, +) + +result = judge0.run( + submissions=user_solution, + interactive_producer=interactive_producer, + test_cases=judge0.TestCase(input="0 100 42 7", expected_output="User successfully guessed the number.\n"), +) + +print(f"Submission status: {result.status}") +print() + +print(f"Producer stdin:\n{result.stdin}") +print(f"Producer stdout:\n{result.stdout}") +print(f"Producer stderr:\n{result.stderr}") +print() + +print(f'User stdin:\n{result.post_execution_filesystem.find("user.stdin")}') +print(f'User stdout:\n{result.post_execution_filesystem.find("user.stdout")}') +print(f'User stderr:\n{result.post_execution_filesystem.find("user.stderr")}') diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index c0150a7..8db6844 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -101,9 +101,10 @@ def _get_implicit_client(flavor: Flavor) -> Client: CE = Flavor.CE EXTRA_CE = Flavor.EXTRA_CE -PYTHON = LanguageAlias.PYTHON +C = LanguageAlias.C CPP = LanguageAlias.CPP -JAVA = LanguageAlias.JAVA -CPP_GCC = LanguageAlias.CPP_GCC CPP_CLANG = LanguageAlias.CPP_CLANG +CPP_GCC = LanguageAlias.CPP_GCC +JAVA = LanguageAlias.JAVA +PYTHON = LanguageAlias.PYTHON PYTHON_FOR_ML = LanguageAlias.PYTHON_FOR_ML diff --git a/src/judge0/api.py b/src/judge0/api.py index 5a81707..8bb42f6 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -1,10 +1,13 @@ from typing import Optional, Union -from .base_types import Flavor, TestCase +from .base_types import Flavor, TestCase, LanguageAlias from .clients import Client from .retry import RegularPeriodRetry, RetryMechanism from .submission import Submission +from .filesystem import Filesystem, File +import textwrap + def resolve_client( client: Optional[Union[Client, Flavor]] = None, @@ -133,6 +136,103 @@ def create_submissions_from_test_cases( return all_submissions +def safe_format(f, arg): + try: + return (f or "") % (arg or "") + except TypeError: + return f or "" + + +def create_fs(client, submission): + fs = Filesystem(submission.additional_files) + language_id = client.get_language_id(submission.language) + if language_id != LanguageAlias.MULTI_FILE: # Multi-file program + language = client.get_language(language_id) + fs.files.append(File(language["source_file"], submission.source_code)) + fs.files.append( + File( + "compile.sh", + safe_format(language["compile_cmd"], submission.compiler_options), + ) + ) + fs.files.append( + File("run.sh", f'{language["run_cmd"]} {submission.command_line_arguments}') + ) + return fs + + +def create_submission_with_interactive_producer( + client, submission, interactive_producer +): + interactive_producer.command_line_arguments = "/box/stdin.fifo /box/stdout.fifo" + + consumer_fs = create_fs(client, submission) + producer_fs = create_fs(client, interactive_producer) + + final_fs = Filesystem() + for f in consumer_fs.files: + final_fs.files.append(File(f"./consumer/{f.name}", f.content)) + + for f in producer_fs.files: + final_fs.files.append(File(f"./producer/{f.name}", f.content)) + + final_fs.files.append( + File( + "compile.sh", + textwrap.dedent( + """ + cd /box/consumer && bash compile.sh + cd /box/producer && bash compile.sh + """ + ), + ) + ) + + final_fs.files.append( + File( + "run.sh", + textwrap.dedent( + """ + mkfifo /box/stdin.fifo /box/stdout.fifo + + cd /box/consumer + tee >(bash run.sh 2> /box/user.stderr | tee /box/stdout.fifo /box/user.stdout &> /dev/null) /box/user.stdin < /box/stdin.fifo &> /dev/null & + + cd /box/producer + bash run.sh < /dev/stdin & + PRODUCER_PID=$! + + wait $PRODUCER_PID + + rm -f /box/stdin.fifo /box/stdout.fifo + """ + ), + ) + ) + + return Submission( + source_code="", + additional_files=final_fs, + language=LanguageAlias.MULTI_FILE, + ) + + +def create_submissions_with_interactive_producer( + client, submissions, interactive_producer +): + if isinstance(submissions, Submission): + return create_submission_with_interactive_producer( + client, submissions, interactive_producer + ) + else: + return [ + create_submission_with_interactive_producer( + client, submission, interactive_producer + ) + for submission in submissions + ] + + def _execute( *, client: Optional[Union[Client, Flavor]] = None, @@ -140,6 +240,7 @@ def _execute( source_code: Optional[str] = None, wait_for_result: bool = False, test_cases: Optional[Union[TestCase, list[TestCase]]] = None, + interactive_producer: Optional[Submission] = None, **kwargs, ) -> Union[Submission, list[Submission]]: if submissions is not None and source_code is not None: @@ -164,6 +265,11 @@ def _execute( client = resolve_client(client, submissions=submissions) + if interactive_producer is not None: + submissions = create_submissions_with_interactive_producer( + client, submissions, interactive_producer + ) + all_submissions = create_submissions_from_test_cases(submissions, test_cases) # We differentiate between creating a single submission and multiple @@ -189,6 +295,7 @@ def async_execute( submissions: Optional[Union[Submission, list[Submission]]] = None, source_code: Optional[str] = None, test_cases: Optional[Union[TestCase, list[TestCase]]] = None, + interactive_producer: Optional[Submission] = None, **kwargs, ) -> Union[Submission, list[Submission]]: return _execute( @@ -197,6 +304,7 @@ def async_execute( source_code=source_code, wait_for_result=False, test_cases=test_cases, + interactive_producer=interactive_producer, **kwargs, ) @@ -207,6 +315,7 @@ def sync_execute( submissions: Optional[Union[Submission, list[Submission]]] = None, source_code: Optional[str] = None, test_cases: Optional[Union[TestCase, list[TestCase]]] = None, + interactive_producer: Optional[Submission] = None, **kwargs, ) -> Union[Submission, list[Submission]]: return _execute( @@ -215,6 +324,7 @@ def sync_execute( source_code=source_code, wait_for_result=True, test_cases=test_cases, + interactive_producer=interactive_producer, **kwargs, ) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 5360b2b..6c3c70a 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -32,6 +32,8 @@ class LanguageAlias(IntEnum): CPP_GCC = 3 CPP_CLANG = 4 PYTHON_FOR_ML = 5 + C = 6 + MULTI_FILE = 89 class Flavor(IntEnum): @@ -54,3 +56,6 @@ class Status(IntEnum): RUNTIME_ERROR_OTHER = 12 INTERNAL_ERROR = 13 EXEC_FORMAT_ERROR = 14 + + def __str__(self): + return self.name.lower().replace("_", " ").title() diff --git a/src/judge0/data.py b/src/judge0/data.py index 1e759c2..5d90197 100644 --- a/src/judge0/data.py +++ b/src/judge0/data.py @@ -7,6 +7,8 @@ LanguageAlias.JAVA: 62, LanguageAlias.CPP_GCC: 54, LanguageAlias.CPP_CLANG: 76, + LanguageAlias.C: 50, + LanguageAlias.MULTI_FILE: 89, }, "1.13.1-extra": { LanguageAlias.PYTHON: 10, @@ -14,6 +16,8 @@ LanguageAlias.JAVA: 4, LanguageAlias.CPP_CLANG: 2, LanguageAlias.PYTHON_FOR_ML: 10, + LanguageAlias.C: 1, + LanguageAlias.MULTI_FILE: 89, }, "1.14.0": { LanguageAlias.PYTHON: 100, @@ -21,6 +25,8 @@ LanguageAlias.JAVA: 91, LanguageAlias.CPP_GCC: 105, LanguageAlias.CPP_CLANG: 76, + LanguageAlias.C: 103, + LanguageAlias.MULTI_FILE: 89, }, "1.14.0-extra": { LanguageAlias.PYTHON: 25, @@ -28,5 +34,7 @@ LanguageAlias.JAVA: 4, LanguageAlias.CPP_CLANG: 2, LanguageAlias.PYTHON_FOR_ML: 25, + LanguageAlias.C: 1, + LanguageAlias.MULTI_FILE: 89, }, } diff --git a/src/judge0/filesystem.py b/src/judge0/filesystem.py index 43e4503..afab1a2 100644 --- a/src/judge0/filesystem.py +++ b/src/judge0/filesystem.py @@ -56,6 +56,16 @@ def encode(self) -> bytes: zip_file.writestr(file.name, file.content) return zip_buffer.getvalue() + def find(self, name: str) -> Optional[File]: + if name.startswith("./"): + name = name[2:] + + for file in self.files: + if file.name == name: + return file + + return None + def __str__(self) -> str: return b64encode(self.encode()).decode()