diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fb99cc..24d0b72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,20 @@ jobs: image_name_tag: hyperskill-go:1.18.2 username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} + build_hyperskill_gcc_image: + name: Build epicbox-hyperskill/gcc image + needs: build_debian_image + runs-on: [ self-hosted, small ] + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Build + uses: ./.github/workflows/actions/build + with: + path: epicbox-hyperskill/gcc + image_name_tag: hyperskill-gcc:10.2.1 + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} build_hyperskill_gradle_image: name: Build epicbox-hyperskill/gradle image runs-on: [self-hosted, small] diff --git a/epicbox-hyperskill/gcc/Dockerfile b/epicbox-hyperskill/gcc/Dockerfile new file mode 100644 index 0000000..bc6f159 --- /dev/null +++ b/epicbox-hyperskill/gcc/Dockerfile @@ -0,0 +1,31 @@ +FROM hyperskill.azurecr.io/epicbox/debian:bullseye + +ENV GCC_VERSION 10.2.1-1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + cmake \ + curl \ + gcc=4:${GCC_VERSION} \ + gcc-multilib=4:${GCC_VERSION} \ + g++=4:${GCC_VERSION} \ + make \ + python3 \ + python3-dev \ + python3-pip \ + unzip \ + && rm -rf /var/lib/apt/lists/* \ + && pip3 install https://github.com/hyperskill/hs-test-python/archive/refs/tags/v11.0.0.tar.gz \ + && mkdir /checker \ + && curl -L -o /checker/kotlin.zip \ + https://github.com/JetBrains/kotlin/releases/download/v1.9.10/kotlin-compiler-1.9.10.zip \ + && unzip /checker/kotlin.zip -d /checker \ + && apt-get remove -y unzip \ + && rm /checker/kotlin.zip \ + && curl -L -o /checker/hs-test.jar \ + https://github.com/hyperskill/hs-test/releases/download/v11.0.0/hs-test-v11.0.0.jar + +ENV PATH="/checker/kotlinc/bin:$PATH" + +COPY checker /checker/ diff --git a/epicbox-hyperskill/gcc/checker/check.sh b/epicbox-hyperskill/gcc/checker/check.sh new file mode 100644 index 0000000..6785ada --- /dev/null +++ b/epicbox-hyperskill/gcc/checker/check.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cd /sandbox +python3 /checker/process.py diff --git a/epicbox-hyperskill/gcc/checker/process.py b/epicbox-hyperskill/gcc/checker/process.py new file mode 100644 index 0000000..751595f --- /dev/null +++ b/epicbox-hyperskill/gcc/checker/process.py @@ -0,0 +1,14 @@ +from process_java import is_java_tests, process_java +from process_python import is_python_tests, process_python +from util import finish_badly, format_exception + +if __name__ == '__main__': + try: + if is_python_tests(): + process_python() + elif is_java_tests(): + process_java() + else: + finish_badly("Cannot find tests for the task") + except Exception as ex: + finish_badly(format_exception(ex)) diff --git a/epicbox-hyperskill/gcc/checker/process_java.py b/epicbox-hyperskill/gcc/checker/process_java.py new file mode 100644 index 0000000..3062faa --- /dev/null +++ b/epicbox-hyperskill/gcc/checker/process_java.py @@ -0,0 +1,97 @@ +import os + +from util import finish, finish_badly, run_process, TASK_ROOT + +HSTEST_JAR = f'/checker/hs-test.jar' +KOTLIN_JAR = f'/checker/kotlinc/lib/kotlin-stdlib.jar' + +CLASSES_FOLDER = f'{TASK_ROOT}/out' +MODULES = [ + f'{TASK_ROOT}/src', + f'{TASK_ROOT}/test', + f'{TASK_ROOT}/util/src', + f'{TASK_ROOT}/util/test', +] + +COMPILE_OPTIONS = [ + '-cp', HSTEST_JAR, '-d', CLASSES_FOLDER +] + +JAVA_EXECUTE = [ + 'java', '-cp', f'{HSTEST_JAR}:{KOTLIN_JAR}:{CLASSES_FOLDER}', '-ea', + f'-DinsideDocker=true', + f'-DignoreStdout=true', + f'-Duser.dir={TASK_ROOT}', + f'-Dfile.encoding=utf-8', + 'org.hyperskill.hstest.stage.StageTest' +] + + +def is_java_tests() -> bool: + tests_folder = f'{TASK_ROOT}/test' + + if not os.path.isdir(tests_folder): + return False + + for path, folders, files in os.walk(tests_folder): + for file in files: + if file.endswith('.java') or file.endswith('.kt'): + return True + + return False + + +def compilation_error_feedback(stderr: str) -> str: + lines = stderr.strip().splitlines() + output = [] + + for line in lines: + if line.startswith(TASK_ROOT): + line = line.replace(TASK_ROOT, '', 1) + output.append(line) + + return 'Compilation error\n\n' + '\n'.join(output).strip() + + +def compile_files(compiler: str, extension: str): + compile_command = [compiler] + COMPILE_OPTIONS + + files_to_compile = [] + + for module in MODULES: + for path, folders, files in os.walk(module): + for file in files: + if file.endswith(extension): + files_to_compile += [os.path.join(path, file)] + + if not files_to_compile: + return + + code, out, err = run_process(compile_command + files_to_compile) + + if code != 0: + finish(False, compilation_error_feedback(out + '\n' + err)) + + +def run_java(): + code, out, err = run_process(JAVA_EXECUTE) + out = out.strip() + err = err.strip() + + if code != 0: + if len(out) == 0 and len(err) == 0: + finish_badly(f'No stdout, no stderr, code = {code}') + + if len(out): + finish(False, out) + + if len(err): + finish(False, err) + + finish(True, '') + + +def process_java(): + compile_files('javac', '.java') + compile_files('kotlinc', '.kt') + run_java() diff --git a/epicbox-hyperskill/gcc/checker/process_python.py b/epicbox-hyperskill/gcc/checker/process_python.py new file mode 100644 index 0000000..7b8b9ff --- /dev/null +++ b/epicbox-hyperskill/gcc/checker/process_python.py @@ -0,0 +1,52 @@ +import os.path + +from util import TASK_ROOT, finish, finish_badly, run_process + +FAILED_TEST_BEGIN = '#educational_plugin FAILED + ' +FAILED_TEST_CONTINUE = '#educational_plugin ' + +TESTS_FILES = [ + f'{TASK_ROOT}/tests.py', + f'{TASK_ROOT}/test/tests.py' +] + + +def is_python_tests() -> bool: + return any(os.path.isfile(f) for f in TESTS_FILES) + + +def process_python(): + test_file = '' + for file in TESTS_FILES: + if os.path.isfile(file): + test_file = file + break + + python_execute_command = [ + 'python3', test_file, '--inside_docker' + ] + + code, out, err = run_process(python_execute_command) + out = out.strip().splitlines() + + if code != 0: + finish_badly(f'Exit code = {code}') + + if any(line.startswith(FAILED_TEST_BEGIN) for line in out): + output = [] + output_started = False + + for line in out: + if output_started and line.startswith(FAILED_TEST_CONTINUE): + output.append(line[len(FAILED_TEST_CONTINUE):]) + + if not output_started and line.startswith(FAILED_TEST_BEGIN): + output_started = True + output.append(line[len(FAILED_TEST_BEGIN):]) + + feedback = '\n'.join(output).strip() + + finish(False, feedback) + + else: + finish(True, '') diff --git a/epicbox-hyperskill/gcc/checker/util.py b/epicbox-hyperskill/gcc/checker/util.py new file mode 100644 index 0000000..d99ab56 --- /dev/null +++ b/epicbox-hyperskill/gcc/checker/util.py @@ -0,0 +1,74 @@ +import json +import subprocess +import sys +import traceback + +TASK_ROOT = '/sandbox' + +all_stdout = [] +all_stderr = [] + + +def finish(successful: bool, feedback: str): + score = 1 if successful else 0 + + if '--debug' in sys.argv: + print(f'Score: {score}\nFeedback:\n{feedback}') + + else: + result = { + 'score': score, + 'feedback': feedback, + } + print(json.dumps(result, sort_keys=True)) + + exit(0) + + +def finish_badly(reason: str = ''): + bad_feedback = ( + 'Cannot check the submission.\n' + '\n' + 'Perhaps your program has fallen into an infinite loop or created too many objects in memory.\n' + 'If you are sure that this is not the case, please send the report to support@hyperskill.org\n' + '\n' + 'reason:\n' + '{reason}\n' + '\n' + 'stdout:\n' + '{stdout}\n' + '\n' + 'stderr:\n' + '{stderr}' + .format(reason=reason, + stdout='\n---\n'.join(all_stdout), + stderr='\n---\n'.join(all_stderr)) + ) + finish(False, bad_feedback) + + +def run_process(args): + proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + exit_code = proc.wait() + + stdout = proc.stdout.read().decode().strip() + stderr = proc.stderr.read().decode().strip() + + all_stdout.append(stdout) + all_stderr.append(stderr) + + return exit_code, stdout, stderr + + +def format_exception(ex): + if sys.version_info >= (3, 10): + traceback_stack = traceback.format_exception(ex) + else: + exc_tb = ex.__traceback__ + traceback_stack = traceback.format_exception(etype=type(ex), value=ex, tb=exc_tb) + + return ''.join(traceback_stack)