From bbef0ac88b2f8cffbc675ce079176f1ad04e582f Mon Sep 17 00:00:00 2001 From: SvizelPritula <32597377+SvizelPritula@users.noreply.github.com> Date: Thu, 15 Dec 2022 22:20:25 +0100 Subject: [PATCH 01/43] fix: pin MarkupSafe to 2.0.1 (#33) (#1211) Co-authored-by: Agus Sentosa Hermawan --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index 9bfcd22fe9..1c54fa38ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,10 @@ babel>=2.6,<2.7 # http://babel.pocoo.org/en/latest/changelog.html pyxdg>=0.26,<0.27 # https://freedesktop.org/wiki/Software/pyxdg/ Jinja2>=2.10,<2.11 # http://jinja.pocoo.org/docs/latest/changelog/ +# See https://github.com/pallets/markupsafe/issues/286 but breaking change in +# MarkupSafe causes jinja to break +MarkupSafe==2.0.1 + # Only for some importers: pyyaml>=5.3,<5.4 # http://pyyaml.org/wiki/PyYAML From 392a441bbab5da9b02665fda2b4fec9a8855fb79 Mon Sep 17 00:00:00 2001 From: Radu Date: Fri, 16 Dec 2022 00:17:53 +0200 Subject: [PATCH 02/43] Updated Romanian translations (#1203) --- cms/locale/ro/LC_MESSAGES/cms.po | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cms/locale/ro/LC_MESSAGES/cms.po b/cms/locale/ro/LC_MESSAGES/cms.po index 609bcd2042..52ff2b54bf 100644 --- a/cms/locale/ro/LC_MESSAGES/cms.po +++ b/cms/locale/ro/LC_MESSAGES/cms.po @@ -465,19 +465,20 @@ msgstr "" msgid "" "Logged in as %(first_name)s %(last_name)s " "(%(username)s)" -msgstr "" +msgstr "Logat ca %(first_name)s %(last_name)s " +"(%(username)s)" msgid "Logout" msgstr "Iesire" msgid "Failed to log in." -msgstr "Eroare de inregistrare" +msgstr "Eroare de logare" msgid "Welcome" msgstr "Bine ati venit" msgid "Please log in" -msgstr "Va rugam sa va inregistrati" +msgstr "Va rugam sa va logati" msgid "Username" msgstr "Nume utilizator" @@ -486,7 +487,7 @@ msgid "Password" msgstr "Parola" msgid "Login" -msgstr "Inregistrare" +msgstr "Logare" msgid "Reset" msgstr "Reseteaza" @@ -825,7 +826,7 @@ msgid "None" msgstr "Lipsa" msgid "Download" -msgstr "Discarca" +msgstr "Descarca" msgid "Played" msgstr "Jucat" From db2701805a63955193761c2b682ae8e7a82784bc Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Sat, 17 Dec 2022 01:39:19 +0100 Subject: [PATCH 03/43] Use docker-compose to run tests (#1221) * Move the testing logic from the Github Actions format to a more easy to reproduce docker-compose file. * Move the codecov report upload logic from CMS to Github Actions. * Set threshold for codecov status check --- .dockerignore | 4 ++ .github/codecov.yml | 5 ++ .github/workflows/main.yml | 77 ++++------------------ .gitignore | 2 + Dockerfile | 53 +++++++++++++++ cmstestsuite/RunFunctionalTests.py | 18 ++--- cmstestsuite/RunUnitTests.py | 16 ++--- cmstestsuite/coverage.py | 102 ++--------------------------- cmstestsuite/testrunner.py | 7 ++ docker-compose.test.yml | 30 +++++++++ 10 files changed, 131 insertions(+), 183 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/codecov.yml create mode 100644 Dockerfile create mode 100644 docker-compose.test.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..ecbeca19d9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.dockerignore +Dockerfile +docker-compose*.yml +.vagrant/ diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000000..236a6f1da9 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + project: + default: + threshold: 1% diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16ae9fc043..0e454f39c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,78 +8,25 @@ jobs: test: runs-on: ubuntu-20.04 - services: - postgres: - image: postgres:12 - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - uses: actions/checkout@v2 with: submodules: true - - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y \ - build-essential \ - php7.4-cli \ - haskell-platform \ - fp-compiler \ - python2.7 \ - cppreference-doc-en-html \ - cgroup-lite \ - libcap-dev \ - python-dev \ - libpq-dev \ - libcups2-dev \ - libyaml-dev \ - rustc \ - mono-mcs - - - name: Install requirements + - name: Check available cgroups run: | - # Use distutils from the standard library for 2to3 translation - export SETUPTOOLS_USE_DISTUTILS="stdlib" - - pip install -U pip setuptools wheel - pip install -r requirements.txt - pip install -r dev-requirements.txt + mount | grep cgroup - sudo $(which python) prerequisites.py -y --cmsuser=$USER install - python setup.py install - - - name: Set up database - env: - PGPASSWORD: postgres + - name: Run tests run: | - psql --host=localhost --username=postgres --command="CREATE USER cmsuser WITH PASSWORD 'your_password_here';" + docker-compose -f docker-compose.test.yml run cms_test - createdb --host=localhost --username=postgres --owner=cmsuser cmsdb - psql --host=localhost --username=postgres --dbname=cmsdb --command='ALTER SCHEMA public OWNER TO cmsuser' - psql --host=localhost --username=postgres --dbname=cmsdb --command='GRANT SELECT ON pg_largeobject TO cmsuser' - - createdb --host=localhost --username=postgres --owner=cmsuser cmsdbfortesting - psql --host=localhost --username=postgres --dbname=cmsdbfortesting --command='ALTER SCHEMA public OWNER TO cmsuser' - psql --host=localhost --username=postgres --dbname=cmsdbfortesting --command='GRANT SELECT ON pg_largeobject TO cmsuser' - - cp ./config/cms.conf.sample ./config/cms.conf - sed -e "s/\"cmsuser\": \"cmsuser\"/\"cmsuser\": \"$USER\"/" -i ./config/cms.conf - - cmsInitDB + - uses: codecov/codecov-action@v3 + with: + files: ./codecov/unittests.xml + flags: unittests - - name: Run tests - run: | - cmsRunTests -v --coverage --codecov + - uses: codecov/codecov-action@v3 + with: + files: ./codecov/functionaltests.xml + flags: functionaltests diff --git a/.gitignore b/.gitignore index 481c85ada1..5b1d9142ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *~ +.DS_Store *.mo .*.sw? *.pyc @@ -11,3 +12,4 @@ cms.egg-info cache/ lib/ log/ +.vagrant/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..81b7d41236 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 +FROM ubuntu:20.04 + +RUN apt-get update +RUN apt-get install -y \ + build-essential \ + cgroup-lite \ + cppreference-doc-en-html \ + fp-compiler \ + git \ + haskell-platform \ + libcap-dev \ + libcups2-dev \ + libffi-dev \ + libpq-dev \ + libyaml-dev \ + mono-mcs \ + openjdk-8-jdk-headless \ + php7.4-cli \ + postgresql-client \ + python2 \ + python3-pip \ + python3.8 \ + python3.8-dev \ + rustc \ + sudo \ + wait-for-it \ + zip + +# Create cmsuser user with sudo privileges +RUN useradd -ms /bin/bash cmsuser && \ + usermod -aG sudo cmsuser +# Disable sudo password +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +# Set cmsuser as default user +USER cmsuser + +COPY --chown=cmsuser:cmsuser . /home/cmsuser/cms + +WORKDIR /home/cmsuser/cms + +RUN sudo pip3 install -r requirements.txt +RUN sudo pip3 install -r dev-requirements.txt +RUN sudo python3 setup.py install + +RUN sudo python3 prerequisites.py --yes --cmsuser=cmsuser install + +RUN sudo sed 's/cmsuser:your_password_here@localhost/postgres@db/' ./config/cms.conf.sample \ + | sudo tee /usr/local/etc/cms-testdb.conf + +ENV LANG C.UTF-8 + +CMD [""] diff --git a/cmstestsuite/RunFunctionalTests.py b/cmstestsuite/RunFunctionalTests.py index db1026db79..0b581ce701 100755 --- a/cmstestsuite/RunFunctionalTests.py +++ b/cmstestsuite/RunFunctionalTests.py @@ -5,6 +5,7 @@ # Copyright © 2013-2018 Stefano Maggiolo # Copyright © 2014 Luca Versari # Copyright © 2016 Luca Wehrstedt +# Copyright © 2022 William Di Luigi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -28,8 +29,7 @@ from cms import utf8_decoder from cmstestsuite import CONFIG from cmstestsuite.Tests import ALL_TESTS -from cmstestsuite.coverage import clear_coverage, combine_coverage, \ - send_coverage_to_codecov +from cmstestsuite.coverage import clear_coverage, combine_coverage from cmstestsuite.profiling import \ PROFILER_KERNPROF, PROFILER_NONE, PROFILER_YAPPI from cmstestsuite.testrunner import TestRunner @@ -168,19 +168,16 @@ def main(): parser.add_argument( "-v", "--verbose", action="count", default=0, help="print debug information (use multiple times for more)") - parser.add_argument( - "--codecov", action="store_true", - help="send coverage results to Codecov (requires --coverage)") g = parser.add_mutually_exclusive_group() g.add_argument( - "--coverage", action="store_true", - help="compute line coverage information") + "--coverage", action="store", type=utf8_decoder, + help="path to the XML coverage report file (if not specified, " + "coverage is not computed)") g.add_argument( "--profiler", choices=[PROFILER_YAPPI, PROFILER_KERNPROF], default=PROFILER_NONE, help="set profiler") + args = parser.parse_args() - if args.codecov and not args.coverage: - parser.error("--codecov requires --coverage") CONFIG["VERBOSITY"] = args.verbose CONFIG["COVERAGE"] = args.coverage @@ -239,9 +236,6 @@ def main(): runner.log_elapsed_time() combine_coverage() - if args.codecov: - send_coverage_to_codecov("functionaltests") - logger.info("Executed: %s", tests) logger.info("Failed: %s", len(failures)) if not failures: diff --git a/cmstestsuite/RunUnitTests.py b/cmstestsuite/RunUnitTests.py index 5a5d40d565..a6fee8b178 100755 --- a/cmstestsuite/RunUnitTests.py +++ b/cmstestsuite/RunUnitTests.py @@ -3,6 +3,7 @@ # Contest Management System - http://cms-dev.github.io/ # Copyright © 2013-2018 Stefano Maggiolo # Copyright © 2016 Luca Wehrstedt +# Copyright © 2022 William Di Luigi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -28,7 +29,7 @@ from cms import utf8_decoder from cmstestsuite import CONFIG, TestException, sh from cmstestsuite.coverage import clear_coverage, combine_coverage, \ - coverage_cmdline, send_coverage_to_codecov + coverage_cmdline from cmstestsuite.profiling import \ PROFILER_KERNPROF, PROFILER_NONE, PROFILER_YAPPI, profiling_cmdline @@ -144,13 +145,11 @@ def main(): "-r", "--retry-failed", action="store_true", help="only run failed tests from the previous run (stored in %s)" % FAILED_UNITTEST_FILENAME) - parser.add_argument( - "--codecov", action="store_true", - help="send coverage results to Codecov (requires --coverage)") g = parser.add_mutually_exclusive_group() g.add_argument( - "--coverage", action="store_true", - help="compute line coverage information") + "--coverage", action="store", type=utf8_decoder, + help="path to the XML coverage report file (if not specified, " + "coverage is not computed)") g.add_argument( "--profiler", choices=[PROFILER_YAPPI, PROFILER_KERNPROF], default=PROFILER_NONE, help="set profiler") @@ -164,8 +163,6 @@ def main(): help="unused") args = parser.parse_args() - if args.codecov and not args.coverage: - parser.error("--codecov requires --coverage") CONFIG["VERBOSITY"] = args.verbose CONFIG["COVERAGE"] = args.coverage @@ -227,9 +224,6 @@ def test_match(t): end_time = datetime.datetime.now() print("Time elapsed: %s" % (end_time - start_time)) - if args.codecov: - send_coverage_to_codecov("unittests") - if passed: return 0 else: diff --git a/cmstestsuite/coverage.py b/cmstestsuite/coverage.py index 2b5d896335..3b3af2b53e 100644 --- a/cmstestsuite/coverage.py +++ b/cmstestsuite/coverage.py @@ -5,7 +5,7 @@ # Copyright © 2013-2018 Stefano Maggiolo # Copyright © 2013-2016 Luca Wehrstedt # Copyright © 2014 Luca Versari -# Copyright © 2014 William Di Luigi +# Copyright © 2014-2022 William Di Luigi # Copyright © 2016 Peyman Jabbarzade Ganje # Copyright © 2017 Luca Chiodini # Copyright © 2021 Andrey Vihrov @@ -23,12 +23,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import os import logging -import requests -import subprocess import sys -from urllib.parse import urlparse from cmstestsuite import CONFIG, sh @@ -50,7 +46,7 @@ def coverage_cmdline(cmdline): """Return a cmdline possibly decorated to record coverage.""" - if CONFIG.get('COVERAGE', False): + if CONFIG.get('COVERAGE', None): return _COVERAGE_CMDLINE + cmdline else: return cmdline @@ -58,100 +54,16 @@ def coverage_cmdline(cmdline): def clear_coverage(): """Clear existing coverage reports.""" - if CONFIG.get('COVERAGE', False): + if CONFIG.get('COVERAGE', None): logging.info("Clearing old coverage data.") sh([sys.executable, "-m", "coverage", "erase"]) def combine_coverage(): """Combine coverage reports from different programs.""" - if CONFIG.get('COVERAGE', False): + coverage_file = CONFIG.get('COVERAGE', None) + + if coverage_file: logger.info("Combining coverage results.") sh([sys.executable, "-m", "coverage", "combine"]) - sh([sys.executable, "-m", "coverage", "xml"]) - - -# Cache directory for subsequent runs. -_CODECOV_DIR = os.path.join("cache", "cmstestsuite", "codecov") - - -def _download_file(url, out): - """Download and save a binary file. - - url (str): file to download. - out (str): output file name. - - """ - r = requests.get(url, stream=True) - r.raise_for_status() - with open(out, "wb") as f: - for chunk in r.iter_content(chunk_size=4096): - f.write(chunk) - - -def _import_pgp_key(gpg_home, keyring, fingerprint): - """Import a PGP key from public keyservers. - - gpg_home (str): GnuPG home directory. - keyring (str): Keyring file to use. - fingerprint (str): PGP key fingerprint. - - """ - - keyservers = [ "hkps://keyserver.ubuntu.com", "hkps://pgp.mit.edu" ] - - for keyserver in keyservers: - logger.info("Importing PGP key %s from %s." % - (fingerprint[-8:], urlparse(keyserver).netloc)) - try: - subprocess.check_call(["gpg", "--homedir", gpg_home, "--keyring", - keyring, "--no-default-keyring", - "--keyserver", keyserver, - "--recv-keys", fingerprint]) - return - except subprocess.CalledProcessError: - logger.warning("PGP key import failed.", exc_info=True) - - raise Exception("No usable keyservers left.") - - -def _get_codecov_uploader(): - """Fetch and return the Codecov uploader. - - return (str): path to the stored uploader. - - """ - base_url = "https://uploader.codecov.io/latest/linux/" - executable = "codecov" - shasum = "codecov.SHA256SUM" - sigfile = "codecov.SHA256SUM.sig" - - gpg_home = os.path.realpath(os.path.join(_CODECOV_DIR, "gnupg")) - # Codecov Uploader (Codecov Uploader Verification Key) - # - fingerprint = "27034E7FDB850E0BBC2C62FF806BB28AED779869" - - if not os.access(os.path.join(_CODECOV_DIR, executable), os.X_OK): - os.makedirs(gpg_home, mode=0o700) - _import_pgp_key(gpg_home, "trustedkeys.gpg", fingerprint) - - logger.info("Fetching Codecov uploader.") - for name in [executable, shasum, sigfile]: - _download_file(base_url + name, os.path.join(_CODECOV_DIR, name)) - - logger.info("Checking Codecov uploader integrity.") - subprocess.check_call(["gpgv", "--homedir", gpg_home, sigfile, shasum], - cwd=_CODECOV_DIR) - subprocess.check_call(["sha256sum", "-c", shasum], cwd=_CODECOV_DIR) - - os.chmod(os.path.join(_CODECOV_DIR, executable), 0o755) - - return os.path.join(_CODECOV_DIR, executable) - - -def send_coverage_to_codecov(flag): - """Send the coverage report to Codecov with the given flag.""" - if CONFIG.get('COVERAGE', False): - logger.info("Sending coverage results to codecov for flag %s." % flag) - uploader = _get_codecov_uploader() - subprocess.check_call([uploader, "-F", flag]) + sh([sys.executable, "-m", "coverage", "xml", "-o", coverage_file]) diff --git a/cmstestsuite/testrunner.py b/cmstestsuite/testrunner.py index dcc4c2a096..68f1c8e648 100644 --- a/cmstestsuite/testrunner.py +++ b/cmstestsuite/testrunner.py @@ -3,6 +3,7 @@ # Contest Management System - http://cms-dev.github.io/ # Copyright © 2015-2018 Stefano Maggiolo # Copyright © 2016 Amir Keivan Mohtashami +# Copyright © 2022 William Di Luigi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -92,6 +93,12 @@ def load_cms_conf(self): CONFIG["CONFIG_PATH"] = "%s/config/cms.conf" % CONFIG["TEST_DIR"] if CONFIG["TEST_DIR"] is None: CONFIG["CONFIG_PATH"] = "/usr/local/etc/cms.conf" + + # Override CMS config path when environment variable is present + CMS_CONFIG_ENV_VAR = "CMS_CONFIG" + if CMS_CONFIG_ENV_VAR in os.environ: + CONFIG["CONFIG_PATH"] = os.environ[CMS_CONFIG_ENV_VAR] + return self.framework.get_cms_config() def log_elapsed_time(self): diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000000..85340d5f88 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,30 @@ +version: "3.3" +services: + db: + container_name: db + image: postgres + environment: + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + # It would be nice here to support OSes that don't have /tmp + # See: https://github.com/docker/compose/issues/3344 + - "/tmp/var_lib_postgresql_data:/var/lib/postgresql/data" + cms_test: + container_name: cms_test + build: . + depends_on: + - "db" + environment: + CMS_CONFIG: /usr/local/etc/cms-testdb.conf + volumes: + - "./codecov:/home/cmsuser/cms/codecov" + privileged: true + command: > + wait-for-it db:5432 -- sh -c " + createdb --host=db --username=postgres cmsdb ; + createdb --host=db --username=postgres cmsdbfortesting ; + cmsInitDB ; + sudo chown cmsuser:cmsuser ./codecov ; + TEST_SUITE=unittests cmsRunTests -v --coverage codecov/unittests.xml ; + TEST_SUITE=functionaltests cmsRunTests -v --coverage codecov/functionaltests.xml ; + " From f541efc94ae0a684f58259c06822e8cad0d4c4be Mon Sep 17 00:00:00 2001 From: Manuel Gundlach Date: Mon, 30 Jan 2023 22:02:50 +0100 Subject: [PATCH 04/43] Handle IntegrityError on file commit gracefully (#1223) Fixes #1214. --- cms/db/filecacher.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cms/db/filecacher.py b/cms/db/filecacher.py index 4bd8ba0e65..e56f28fc5e 100644 --- a/cms/db/filecacher.py +++ b/cms/db/filecacher.py @@ -7,6 +7,7 @@ # Copyright © 2013 Luca Wehrstedt # Copyright © 2016 Luca Versari # Copyright © 2021 Fabian Gundlach <320pointsguy@gmail.com> +# Copyright © 2023 Manuel Gundlach # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -362,8 +363,12 @@ def commit_file(self, fobj, digest, desc=""): # If someone beat us to adding the same object to the database, we # should at least drop the large object. LargeObject.unlink(fobj.loid) - logger.warning("File %s (%s) caused an IntegrityError, ignoring.", - digest, desc) + logger.info("File %s (%s) caused an IntegrityError, ignoring. " + "Note: If the file is a compiled submission " + "executable, this can occur when a contestant has " + "submitted identical source code multiple times, " + "and is harmless in that case.", + digest, desc) return False return True From 7d962150d9969281148d3eaba1ab35823fdfb1b6 Mon Sep 17 00:00:00 2001 From: Anton Tsypko Date: Mon, 6 Feb 2023 23:36:12 +0100 Subject: [PATCH 05/43] Added Ukrainian translation (#1225) --- cms/locale/uk/LC_MESSAGES/cms.po | 1257 ++++++++++++++++++++++++++++++ 1 file changed, 1257 insertions(+) create mode 100644 cms/locale/uk/LC_MESSAGES/cms.po diff --git a/cms/locale/uk/LC_MESSAGES/cms.po b/cms/locale/uk/LC_MESSAGES/cms.po new file mode 100644 index 0000000000..30f6401e25 --- /dev/null +++ b/cms/locale/uk/LC_MESSAGES/cms.po @@ -0,0 +1,1257 @@ +# Translations template for Contest Management System. +# Copyright (C) 2023 CMS development group +# This file is distributed under the same license as the Contest Management +# System project. +# Anton Tsypko tsypko@oi.in.ua, 2023. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Contest Management System 1.5.dev0\n" +"Report-Msgid-Bugs-To: contestms@googlegroups.com\n" +"MIME-Version: 1.0\n" +"Last-Translator: Anton Tsypko \n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Project-Id-Version: c\n" +"Language: uk\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: +msgid "N/A" +msgstr "N/A" + +#: +msgid "Not correct" +msgstr "Неправильно" + +#: +msgid "Correct" +msgstr "Правильно" + +#: +msgid "Partially correct" +msgstr "Частково правильно" + +#: +msgid "#" +msgstr "#" + +#: +msgid "Outcome" +msgstr "Результат" + +#: +msgid "Details" +msgstr "Деталі" + +#: +msgid "Execution time" +msgstr "Час виконання" + +#: +msgid "Memory used" +msgstr "Використана пам'ять" + +#: +msgid "Score details temporarily unavailable." +msgstr "Деталі результатів тимчасово недоступні." + +#: +msgid "Subtask %(index)s" +msgstr "Підзавдання %(index)s" + +#: +msgid "Compilation succeeded" +msgstr "Компіляція виконана успішно" + +#: +msgid "Your submission successfully compiled to an executable." +msgstr "Ваша спроба успішно скомпільована у виконуваний файл." + +#: +msgid "Compilation failed" +msgstr "Помилка компіляції" + +#: +msgid "Your submission did not compile correctly." +msgstr "Ваша спроба скомпільована неправильно." + +#: +msgid "Compilation timed out" +msgstr "Час очікування компіляції минув" + +#: +msgid "Your submission exceeded the time limit while compiling. This might be caused by an excessive use of C++ templates, for example." +msgstr "Ви перевищили ліміт часу під час компіляції. Це може бути спричинено, наприклад, надмірним використанням шаблонів C++." + +#: +msgid "Compilation killed with signal %s (could be triggered by violating memory limits)" +msgstr "Компіляція припинена сигналом %s (може бути викликано порушенням обмежень пам’яті)" + +#: +msgid "Your submission was killed with the specified signal. Among other things, this might be caused by exceeding the memory limit for the compilation, and in turn by an excessive use of C++ templates, for example." +msgstr "Ваша спроба була знищена вказаним сигналом. Серед іншого, це може бути спричинено перевищенням обмеження пам’яті для компіляції та, у свою чергу, надмірним використанням шаблонів C++, наприклад." + +#: +msgid "Output is correct" +msgstr "Вихідні дані правильні" + +#: +msgid "Your submission ran and gave the correct answer" +msgstr "Ваша спроба запущена та дала правильну відповідь" + +#: +msgid "Output is partially correct" +msgstr "Вихідні дані частково правильні" + +#: +msgid "Your submission ran and gave the partially correct answer" +msgstr "Ваша спроба запущена та дала частково правильну відповідь" + +#: +msgid "Output isn't correct" +msgstr "Вихідні дані неправильні" + +#: +msgid "Your submission ran, but gave the wrong answer" +msgstr "Ваша спроба запущена, але дала неправильну відповідь" + +#: +msgid "Evaluation didn't produce file %s" +msgstr "Оцінювання не створило файл %s" + +#: +msgid "Your submission ran, but did not write on the correct output file" +msgstr "Ваша спроба запущена, але не вивела нічого у правильний вихідний файл" + +#: +msgid "Execution timed out" +msgstr "Час запуску минув" + +#: +msgid "Your submission used too much CPU time." +msgstr "Ваша спроба використала забагато процесорного часу." + +#: +msgid "Execution timed out (wall clock limit exceeded)" +msgstr "Час очікування минув (перевищено ліміт настінного годинника)" + +#: +msgid "Your submission used too much total time. This might be triggered by undefined code, or buffer overflow, for example. Note that in this case the CPU time visible in the submission details might be much smaller than the time limit." +msgstr "Ваша спроба використала забагато загального часу. Це може бути викликано, наприклад, невизначеним кодом або переповненням буфера. Зауважте, що в цьому випадку час процесора, видимий у деталях спроби, може бути набагато меншим за обмеження часу." + +#: +msgid "Execution killed (could be triggered by violating memory limits)" +msgstr "Виконання припинено (може бути викликано порушенням обмежень пам’яті)" + +#: +msgid "The evaluation was killed by a signal. Among other things, this might be caused by exceeding the memory limit. Note that if this is the reason, the memory usage visible in the submission details is the usage before the allocation that caused the signal." +msgstr "Оцінювання було знищено сигналом. Серед іншого це може бути спричинено перевищенням ліміту пам’яті. Зауважте, що якщо це причина, використання пам’яті, видиме в деталях спроби, є використанням до розподілу, який спричинив сигнал." + +#: +msgid "Execution failed because the return code was nonzero" +msgstr "Помилка виконання, оскільки код повернення був ненульовим" + +#: +msgid "Your submission failed because it exited with a return code different from 0." +msgstr "Ваше надсилання не виконано, оскільки він завершився з кодом повернення, відмінним від 0." + +#: +msgid "Execution completed successfully" +msgstr "Виконання успішно завершено" + +#: +msgid "No compilation needed" +msgstr "Компіляція не потрібна" + +#: +msgid "File not submitted" +msgstr "Файл не надіслано" + +#: +msgid "Question too long!" +msgstr "Питання занадто довге!" + +#: +msgid "Subject must be at most %(max_subject_length)d characters, content at most %(max_text_length)d." +msgstr "Тема має містити щонайбільше %(max_subject_length)d символів, а вміст – не більше %(max_text_length)d." + +#: +msgid "contest-token" +msgstr "токен змагання" + +#: +msgid "contest-tokens" +msgstr "токени змагання" + +#: +msgid "task-token" +msgstr "токен завдання" + +#: +msgid "task-tokens" +msgstr "токени завдання" + +#: +msgid "token" +msgstr "токен" + +#: +msgid "tokens" +msgstr "токени" + +#: +msgid "You don't have %(type_pl)s available for this task." +msgstr "У вас немає %(type_pl)s для цього завдання." + +#: +msgid "You have an infinite number of %(type_pl)s for this task." +msgstr "У вас є нескінченна кількість %(type_pl)s для цього завдання." + +#: +msgid "You start with no %(type_pl)s." +msgstr "Ви починаєте без %(type_pl)s." + +#: +msgid "You start with one %(type_s)s." +msgid_plural "You start with %(gen_initial)d %(type_pl)s." +msgstr[0] "Ви починаєте з одного %(type_s)s." +msgstr[1] "Ви починаєте з двох %(type_s)s." +msgstr[2] "Ви починаєте з %(gen_initial)d %(type_s)s." + +#: +msgid "Every minute " +msgid_plural "Every %(gen_interval)g minutes " +msgstr[0] "Кожну хвилину " +msgstr[1] "Кожні %(gen_interval)g хвилини " +msgstr[2] "Кожні %(gen_interval)g хвилин " + +#: +msgid "you get another %(type_s)s, " +msgid_plural "you get %(gen_number)d other %(type_pl)s, " +msgstr[0] "ви отримуєте інший %(type_s)s, " +msgstr[1] "ви отримуєте два інші %(type_s)s, " +msgstr[2] "ви отримуєте %(gen_number)d інших %(type_s)s, " + + +#: +msgid "up to a maximum of one %(type_s)s." +msgid_plural "up to a maximum of %(gen_max)d %(type_pl)s." +msgstr[0] "до максимум одного %(type_s)s." +msgstr[1] "до максимум двох %(type_s)s. " +msgstr[2] "до максимум %(gen_max)d %(type_s)s. " + +#: +msgid "you get another %(type_s)s." +msgid_plural "you get %(gen_number)d other %(type_pl)s." +msgstr[0] "ви отримуєте інший %(type_s)s." +msgstr[1] "ви отримуєте інші два %(type_s)s." +msgstr[2] "ви отримуєте інших %(gen_number)d %(type_s)s." + +#: +msgid "You don't get other %(type_pl)s." +msgstr "Ви не отримуєте інших %(type_pl)s." + +#: +msgid "You can use a %(type_s)s every second " +msgid_plural "You can use a %(type_s)s every %(min_interval)g seconds " +msgstr[0] "Ви можете використовувати %(type_s)s щосекунди " +msgstr[1] "Ви можете використовувати %(type_s)s кожні дві секунди " +msgstr[2] "Ви можете використовувати %(type_s)s кожні %(min_interval)g секунд " + +#: +msgid "and no more than one %(type_s)s in total." +msgid_plural "and no more than %(max_number)d %(type_pl)s in total." +msgstr[0] "і не більше одного %(type_s)s загалом." +msgstr[1] "і не більше двух %(type_s)s загалом" +msgstr[2] "і не більше %(max_number)d %(type_s)s загалом" + +#: +msgid "You can use a %(type_s)s every second." +msgid_plural "You can use a %(type_s)s every %(min_interval)g seconds." +msgstr[0] "Ви можете використовувати %(type_s)s щосекунди." +msgstr[1] "Ви можете використовувати %(type_s)s кожні дві секунди." +msgstr[2] "Ви можете використовувати %(type_s)s кожні %(min_interval)g секунд." + +#: +msgid "You can use no more than one %(type_s)s in total." +msgid_plural "You can use no more than %(max_number)d %(type_pl)s in total." +msgstr[0] "Загалом ви можете використовувати не більше одного %(type_s)s." +msgstr[1] "Загалом ви можете використовувати не більше двох %(type_s)s." +msgstr[2] "Загалом ви можете використовувати не більше %(max_number)d %(type_s)s." + +#: +msgid "You have no limitations on how you use them." +msgstr "Ви не маєте обмежень щодо того, як ви їх використовуєте." + +#: +msgid "Too many print jobs!" +msgstr "Забагато завдань для друку!" + +#: +msgid "You have reached the maximum limit of at most %d print jobs." +msgstr "Ви досягли максимальної кількості завдань для друку (%d)." + +#: +msgid "Invalid format!" +msgstr "Недійсний формат!" + +#: +msgid "Please select the correct files." +msgstr "Виберіть правильні файли." + +#: +msgid "File too big!" +msgstr "Файл завеликий!" + +#: +msgid "Each file must be at most %d bytes long." +msgstr "Довжина кожного файлу не повинна перевищувати %d байтів." + +#: +msgid "Print job storage failed!" +msgstr "Не вдалося зберегти завдання для друку!" + +#: +msgid "Please try again." +msgstr "Будь ласка спробуйте ще раз." + +#: +msgid "Token request discarded" +msgstr "Запит токена відхилено" + +#: +msgid "Your request has been discarded because you have no tokens available." +msgstr "Ваш запит відхилено, оскільки у вас немає доступних токенів." + +#: +msgid "Your request has been discarded because you already used a token on that submission." +msgstr "Ваш запит відхилено, оскільки ви вже використали токен для цієї спроби." + +#: +msgid "Question received" +msgstr "Питання отримано" + +#: +msgid "Your question has been received, you will be notified when it is answered." +msgstr "Ваше запитання отримано, Ви отримаєте сповіщення, коли на нього дадуть відповідь." + +#: +msgid "Print job received" +msgstr "Отримано завдання для друку" + +#: +msgid "Your print job has been received." +msgstr "Ваше завдання для друку отримано." + +#: +msgid "Submission received" +msgstr "Спробу отримано" + +#: +msgid "Your submission has been received and is currently being evaluated." +msgstr "Ваша спроба отримана та наразі оцінюється." + +#: +msgid "Compiling..." +msgstr "Компіляція..." + +#: +msgid "Evaluating..." +msgstr "Оцінювання..." + +#: +msgid "Scoring..." +msgstr "Бали..." + +#: +msgid "Evaluated" +msgstr "Оцінено" + +#: +msgid "status" +msgstr "статус" + +#: +msgid "Token request received" +msgstr "Отримано запит на токен" + +#: +msgid "Your request has been received and applied to the submission." +msgstr "Ваш запит отримано та застосовано до спроби." + +#: +msgid "Test received" +msgstr "Тест отримано" + +#: +msgid "Your test has been received and is currently being executed." +msgstr "Ваш тест отримано та зараз виконується." + +#: +msgid "details" +msgstr "деталі" + +#: +msgid "Executing..." +msgstr "Виконання..." + +#: +msgid "Executed" +msgstr "Виконано" + +#: +msgid "Too many submissions!" +msgstr "Забагато спроб!" + +#: +msgid "You have reached the maximum limit of at most %d submissions among all tasks." +msgstr "Ви досягли максимального ліміту (не більше ніж %d спроб) для всіх завдань." + +#: +msgid "You have reached the maximum limit of at most %d submissions on this task." +msgstr "Ви досягли максимального ліміту (не більше ніж %d спроб) для цього завдання." + +#: +msgid "Submissions too frequent!" +msgstr "Спроби надто часті!" + +#: +msgid "Among all tasks, you can submit again after %d seconds from last submission." +msgstr "Серед усіх завдань ви можете надіслати знову через %d секунд після останньої спроби." + +#: +msgid "For this task, you can submit again after %d seconds from last submission." +msgstr "Це завдання можна надіслати знову через %d секунд після останньої спроби." + +#: +msgid "Invalid archive format!" +msgstr "Невірний формат архіву!" + +#: +msgid "The submitted archive could not be opened." +msgstr "Не вдалося відкрити надісланий архів." + +#: +msgid "Invalid submission format!" +msgstr "Недійсний формат спроби!" + +#: +msgid "Submission too big!" +msgstr "Спроба занадто велика!" + +#: +msgid "Each source file must be at most %d bytes long." +msgstr "Довжина кожного вихідного файлу не повинна перевищувати %d байтів." + +#: +msgid "Submission storage failed!" +msgstr "Помилка зберігання даних!" + +#: +msgid "Too many tests!" +msgstr "Забагато тестів!" + +#: +msgid "You have reached the maximum limit of at most %d tests among all tasks." +msgstr "Ви досягли максимального ліміту не більше %d тестів серед усіх завдань." + +#: +msgid "You have reached the maximum limit of at most %d tests on this task." +msgstr "Ви досягли максимального ліміту щонайбільше %d тестів для цього завдання." + +#: +msgid "Tests too frequent!" +msgstr "Тести занадто часті!" + +#: +msgid "Among all tasks, you can test again after %d seconds from last test." +msgstr "Серед усіх завдань ви можете повторити тестування через %d секунд після останнього тесту." + +#: +msgid "For this task, you can test again after %d seconds from last test." +msgstr "Ви можете повторити це завдання через %d секунди після останнього тесту." + +#: +msgid "Invalid test format!" +msgstr "Недійсний формат тесту!" + +#: +msgid "Test too big!" +msgstr "Тест занадто великий!" + +#: +msgid "Input too big!" +msgstr "Вхідні дані завеликі!" + +#: +msgid "The input file must be at most %d bytes long." +msgstr "Вхідний файл має мати щонайбільше %d байт." + +#: +msgid "Test storage failed!" +msgstr "Перевірити пам'ять не вдалося!" + +#: +msgid "Communication" +msgstr "Взаємодія" + +#: +msgid "Announcements" +msgstr "Оголошення" + +#: +msgid "(no subject)" +msgstr "(без теми)" + +#: +msgid "Questions" +msgstr "Питання" + +#: +msgid "Subject" +msgstr "Тема" + +#: +msgid "Text" +msgstr "Текст" + +#: +msgid "Ask question" +msgstr "Поставте запитання" + +#: +msgid "Reset" +msgstr "Скинути" + +#: +msgid "no answer yet" +msgstr "ще немає відповіді" + +#: +msgid "Messages" +msgstr "Повідомлення" + +#: +msgid "Automatic (%(lang)s)" +msgstr "Автоматично (%(lang)s)" + +#: +msgid "Logout" +msgstr "Вийти" + +#: +msgid "Logged in as %(first_name)s %(last_name)s (%(username)s)" +msgstr "Ви ввійшли як %(first_name)s %(last_name)s (%(username)s)" + +#: +msgid "Failed to log in." +msgstr "Не вдалося увійти." + +#: +msgid "Welcome" +msgstr "Ласкаво просимо" + +#: +msgid "Please log in" +msgstr "Будь ласка, увійдіть" + +#: +msgid "Username" +msgstr "Логін" + +#: +msgid "Password" +msgstr "Пароль" + +#: +msgid "Login" +msgstr "Увійти" + +#: +msgid "Don't have an account?" +msgstr "Немає облікового запису?" + +#: +msgid "Register" +msgstr "зареєструватися" + +#: +msgid "New message" +msgstr "Нове повідомлення" + +#: +msgid "New announcement" +msgstr "Нове оголошення" + +#: +msgid "New answer" +msgstr "Нова відповідь" + +#: +msgid "%d unread" +msgstr "%d непрочитаних" + +#: +msgid "Until contest starts:" +msgstr "До початку змагання:" + +#: +msgid "Until contest ends:" +msgstr "До закінчення змагання:" + +#: +msgid "Until analysis starts:" +msgstr "До початку аналізу:" + +#: +msgid "Until analysis ends:" +msgstr "До закінчення аналізу:" + +#: +msgid "Time left:" +msgstr "Залишився час:" + +#: +msgid "Server time:" +msgstr "Час сервера:" + +#: +msgid "Overview" +msgstr "Огляд" + +#: +msgid "Statement" +msgstr "Умова" + +#: +msgid "Submissions" +msgstr "Спроби" + +#: +msgid "Documentation" +msgstr "Документація" + +#: +msgid "Testing" +msgstr "Тестування" + +#: +msgid "Printing" +msgstr "Друк" + +#: +msgid "Contest Management System" +msgstr "Contest Management System" + +#: +msgid "is released under the" +msgstr "випускається під" + +#: +msgid "GNU Affero General Public License" +msgstr "GNU Affero General Public License" + +#: +msgid "Choose a contest" +msgstr "Виберіть змагання" + +#: +msgid "Programming languages and libraries" +msgstr "Мови програмування та бібліотеки" + +#: +msgid "Standard Template Library" +msgstr "Стандартна бібліотека шаблонів" + +#: +msgid "The main Java class of the solution should have exactly the same name as the task." +msgstr "Основний клас Java рішення повинен мати точно таке ж ім’я, як і завдання." + +#: +msgid "Submission details for compilation" +msgstr "Деталі спроби для компіляції" + +#: +msgid "Message" +msgstr "Повідомлення" + +#: +msgid "Explanation" +msgstr "Пояснення" + +#: +msgid "Submission details for evaluation" +msgstr "Деталі спроби для оцінювання" + +#: +msgid "Error %(status_code)s" +msgstr "Помилка %(status_code)s" + +#: +msgid "An error occured while the server was handling your request." +msgstr "Під час обробки вашого запиту сервером сталася помилка." + +#: +msgid "Note that attempts to tamper with Contest Management System (such as probing the server with customized URLs) may be considered cheating and may lead to disqualification." +msgstr "Зауважте, що спроби підробити систему керування змаганнями (наприклад, перевірка сервера за допомогою налаштованих URL-адрес) можуть вважатися шахрайством і призвести до дискваліфікації." + +#: +msgid "If you encountered this error during normal usage, please notify the contest administrators." +msgstr "Якщо ви зіткнулися з цією помилкою під час звичайного використання, повідомте про це адміністраторів змагання." + +#: +msgid "General information" +msgstr "Загальна інформація" + +#: +msgid "The contest hasn't started yet." +msgstr "Змагання ще не розпочалося." + +#: +msgid "The contest will start at %(start_time)s and will end at %(stop_time)s." +msgstr "Змагання розпочнеться о %(start_time)s і закінчиться о %(stop_time)s." + +#: +msgid "The contest is currently running." +msgstr "Наразі змагання триває." + +#: +msgid "The contest started at %(start_time)s and will end at %(stop_time)s." +msgstr "Змагання розпочалося о %(start_time)s і завершиться о %(stop_time)s." + +#: +msgid "The contest has already ended." +msgstr "Змагання вже завершилося." + +#: +msgid "The contest started at %(start_time)s and ended at %(stop_time)s." +msgstr "Змагання розпочалося о %(start_time)s і завершилося о %(stop_time)s." + +#: +msgid "The analysis mode hasn't started yet." +msgstr "Режим аналізу ще не почався." + +#: +msgid "The analysis mode will start at %(start_time)s and will end at %(stop_time)s." +msgstr "Режим аналізу розпочнеться о %(start_time)s і закінчиться о %(stop_time)s." + +#: +msgid "The analysis mode is currently running." +msgstr "Зараз працює режим аналізу." + +#: +msgid "The analysis mode started at %(start_time)s and will end at %(stop_time)s." +msgstr "Режим аналізу почався о %(start_time)s і закінчиться о %(stop_time)s." + +#: +msgid "The analysis mode has already ended." +msgstr "Режим аналізу вже закінчився." + +#: +msgid "The analysis mode started at %(start_time)s and ended at %(stop_time)s." +msgstr "Режим аналізу почався о %(start_time)s і закінчився о %(stop_time)s." + +#: +msgid "You have an infinite number of tokens." +msgstr "У вас є нескінченна кількість токенів." + +#: +msgid "You can see the detailed result of a submission by using a token on it." +msgstr "Ви можете побачити детальний результат спроби, використовуючи токен на ньому." + +#: +msgid "Your score for each task will be the maximum among the tokened submissions and the last one." +msgstr "Ваш бал за кожне завдання буде максимальним серед спроб (з токеном) і останнього." + +#: +msgid "You have a distinct set of tokens for each task." +msgstr "У вас є окремий набір токенів для кожного завдання." + +#: +msgid "You can find the rules for the %(type_pl)s on each task's description page." +msgstr "Ви можете знайти правила для %(type_pl)s на сторінці опису кожного завдання." + +#: +msgid "You have a set of tokens shared among all tasks." +msgstr "У вас є набір токенів, спільний для всіх завдань." + +#: +msgid "You have two types of tokens: a set of contest-tokens shared among all tasks and a distinct set of task-tokens for each task." +msgstr "У вас є два типи токенів: набір токенів для змагання, спільний для всіх завдань, і окремий набір токен для завдання для кожного завдання." + +#: +msgid "You can see the detailed result of a submission by using two tokens on it, one of each type." +msgstr "Ви можете побачити детальний результат спроби, використовуючи два токени на ньому, по одному кожного типу." + +#: +msgid "You can submit at most %(submissions)s solutions during this contest." +msgstr "Під час цього змагання ви можете надіслати щонайбільше %(submissions)s рішень." + +#: +msgid "You can submit at most %(user_tests)s user tests during this contest." +msgstr "Під час цього змагання ви можете надіслати щонайбільше %(user_tests)s тестів користувача." + +#: +msgid "Every user is allowed to compete (i.e. submit solutions) for a uninterrupted time frame of %(per_user_time)s." +msgstr "Кожному користувачеві дозволено змагатися (тобто надсилати рішення) протягом безперервного проміжку часу %(per_user_time)s." + +#: +msgid "As soon as the contest starts you can choose to start your time frame." +msgstr "Як тільки змагання розпочнеться, ви зможете розпочати свій часовий проміжок." + +#: +msgid "Once you start, you can submit solutions until the end of the time frame or until the end of the contest, whatever comes first." +msgstr "Почавши, ви можете надсилати рішення до кінця відведеного часового проміжку або до кінця змагання, залежно від того, що відбудеться раніше." + +#: +msgid "By clicking on the button below you can start your time frame." +msgstr "Натиснувши на кнопку нижче, ви можете запустити свій часовий проміжок." + +#: +msgid "You started your time frame at %(start_time)s." +msgstr "Ви запустили свій часовий проміжок о %(start_time)s." + +#: +msgid "You can submit solutions until the end of the time frame or until the end of the contest, whatever comes first." +msgstr "Ви можете надсилати рішення до кінця відведеного часового проміжку або до кінця змагання, залежно від того, що відбудеться раніше." + +#: +msgid "You started your time frame at %(start_time)s and you already finished it." +msgstr "Ви запустили свій часовий проміжок о %(start_time)s і вже завершили його." + +#: +msgid "There's nothing you can do now." +msgstr "Ви нічого не можете зробити зараз." + +#: +msgid "You never started your time frame. Now it's too late." +msgstr "Ви ніколи не запускали свій часовий проміжок. Тепер уже пізно." + +#: +msgid "Start!" +msgstr "Старт!" + +#: +msgid "Task overview" +msgstr "Огляд завдання" + +#: +msgid "Task" +msgstr "Завдання" + +#: +msgid "Name" +msgstr "Ім'я" + +#: +msgid "Time limit" +msgstr "Обмеження часу" + +#: +msgid "Memory limit" +msgstr "Обмеження пам'яті" + +#: +msgid "Type" +msgstr "Тип" + +#: +msgid "Files" +msgstr "Файли" + +#: +msgid "Tokens" +msgstr "Токени" + +#: +msgid "Yes" +msgstr "Так" + +#: +msgid "No" +msgstr "Ні" + +#: +msgid "Print" +msgstr "Роздрукувати" + +#: +msgid "You can print %(remaining_jobs)s more text or PDF files of up to %(max_pages)s pages each." +msgstr "Ви можете надрукувати ще %(remaining_jobs)s текстових або PDF-файлів обсягом до %(max_pages)s сторінок кожен." + +#: +msgid "You can print %(remaining_jobs)s more text files of up to %(max_pages)s pages each." +msgstr "Ви можете надрукувати ще %(remaining_jobs)s текстових файлів обсягом до %(max_pages)s сторінок кожен." + +#: +msgid "File (text or PDF)" +msgstr "Файл (текст або PDF)" + +#: +msgid "File (text)" +msgstr "Файл (текст)" + +#: +msgid "Submit" +msgstr "Надіслати" + +#: +msgid "You cannot print anything any more as you have used up your printing quota." +msgstr "Ви більше не можете нічого друкувати, оскільки ви використали свою квоту друку." + +#: +msgid "Previous print jobs" +msgstr "Попередні завдання для друку" + +#: +msgid "Date and time" +msgstr "Дата і час" + +#: +msgid "Time" +msgstr "Час" + +#: +msgid "File name" +msgstr "Ім'я файлу" + +#: +msgid "Status" +msgstr "Статус" + +#: +msgid "Preparing..." +msgstr "Підготовка..." + +#: +msgid "no print jobs yet" +msgstr "ще немає завдань для друку" + +#: +msgid "The passwords do not match!" +msgstr "Паролі не збігаються!" + +#: +msgid "This username is already taken, please choose a different one." +msgstr "Цей логін вже зайнятий, виберіть інший." + +#: +msgid "This user is already registered in the contest." +msgstr "Цей користувач вже зареєстрований у змаганні." + +#: +msgid "No such user." +msgstr "Немає такого користувача." + +#: +msgid "The password is not correct." +msgstr "Пароль неправильний." + +#: +msgid "Registration" +msgstr "Реєстрація" + +#: +msgid "Please fill in the fields to register" +msgstr "Будь ласка, заповніть поля для реєстрації" + +#: +msgid "New user" +msgstr "Новий користувач" + +#: +msgid "Join contest" +msgstr "Приєднатися до змагання" + +#: +msgid "First name" +msgstr "Ім'я" + +#: +msgid "Last name" +msgstr "Прізвище" + +#: +msgid "E-mail" +msgstr "Електронна пошта" + +#: +msgid "Representing" +msgstr "Представляючи" + +#: +msgid "Must be one character or more." +msgid_plural "Must be %(min_length)s characters or more." +msgstr[0] "Має бути один або більше символів." +msgstr[1] "Має бути два або більше символів." +msgstr[2] "Має бути принаймні %(min_length)s символів." + +#: +msgid "Confirm password" +msgstr "Підтвердьте пароль" + +#: +msgid "Registered in the contest successfully!" +msgstr "Успішна реєстрація в змаганні!" + +#: +msgid "Your username is:" +msgstr "Ваш логін:" + +#: +msgid "Your password is stored securely." +msgstr "Ваш пароль надійно зберігається." + +#: +msgid "Back to login" +msgstr "Назад до входу" + +#: +msgid "Compilation output" +msgstr "Вихід компіляції" + +#: +msgid "Compilation outcome:" +msgstr "Результат компіляції:" + +#: +msgid "Compilation time:" +msgstr "Час компіляції:" + +#: +msgid "Memory used:" +msgstr "Використана пам'ять:" + +#: +msgid "Standard output" +msgstr "Стандартний вихід" + +#: +msgid "Standard error" +msgstr "Стандартна помилка" + +#: +msgid "%(name)s (%(short_name)s) description" +msgstr "%(name)s (%(short_name)s) опис" + +#: +msgid "no statement available" +msgstr "немає умови" + +#: +msgid "Download task statement" +msgstr "Завантажити умову завдання" + +#: +msgid "The statement for this task is available in multiple versions, in different languages." +msgstr "Інструкція для цього завдання доступна в кількох версіях різними мовами." + +#: +msgid "You can see (and download) all of them using the list on the right." +msgstr "Ви можете переглянути (і завантажити) усі з них, використовуючи список праворуч." + +#: +msgid "Some suggested translations follow." +msgstr "Нижче наведено деякі запропоновані переклади." + +#: +msgid "Statement in %(lang)s" +msgstr "Умови мовою %(lang)s" + +#: +msgid "Statement in %(lang)s" +msgstr "Умова %(lang)s" + +#: +msgid "%(lang)s" +msgstr "%(lang)s" + +#: +msgid "%(lang)s" +msgstr "%(lang)s" + +#: +msgid "Some details" +msgstr "Деякі деталі" + +#: +msgid "Compilation commands" +msgstr "Команди компіляції" + +#: +msgid "You can find the rules for the %(type_pl)s on the contest overview page." +msgstr "Ви можете знайти правила для %(type_pl)s на сторінці огляду змагання." + +#: +msgid "Remember that to see the detailed result of a submission you need to use both a contest-token and a task-token." +msgstr "Пам’ятайте, що для перегляду детальних результатів спроби потрібно використовувати як токен змагання, так і токен завдання." + +#: +msgid "Attachments" +msgstr "Додатки" + +#: +msgid "unknown" +msgstr "невідомий" + +#: +msgid "loading..." +msgstr "завантаження..." + +#: +msgid "%(name)s (%(short_name)s) submissions" +msgstr "%(name)s (%(short_name)s) спроби" + +#: +msgid "Score:" +msgstr "Бал:" + +#: +msgid "Public score:" +msgstr "Публічний бал:" + +#: +msgid "Score of tokened submissions:" +msgstr "Бали позначених спроб:" + +#: +msgid "Total score:" +msgstr "Загальний бал:" + +#: +msgid "Submit a solution" +msgstr "Надішліть рішення" + +#: +msgid "You may submit any subset of outputs in a single submission." +msgstr "Ви можете надіслати будь-яку підмножину виводів в одній спробі." + +#: +msgid "You can submit %(submissions_left)s more solution(s)." +msgstr "Ви можете надіслати ще %(submissions_left)s рішення(ів)." + +#: +msgid "submission.zip" +msgstr "submission.zip" + +#: +msgid "Previous submissions" +msgstr "Попередні спроби" + +#: +msgid "Tokens are not allowed on this task." +msgstr "У цьому завданні токени заборонені." + +msgid "Right now, you have infinite tokens available on this task." +msgstr "Зараз у вас є нескінченні токени для цієї задачі." + +msgid "Right now, you have one token available on this task." +msgstr "Зараз у вас є один токен для цієї задачі." + +#, python-format +msgid "Right now, you have %(tokens)s tokens available on this task." +msgstr "Зараз у вас є %(tokens)s токенів для цієї задачі." + +#, python-format +msgid "But you have to wait until %(expiration_time)s to use them." +msgstr "Але вам доведеться почекати до %(expiration_time)s, щоб використати їх." + +#, python-format +msgid "You will receive a new token at %(gen_time)s." +msgstr "Ви отримаєте новий токен в %(gen_time)s." + +msgid "In the current situation, no more tokens will be generated." +msgstr "У поточній ситуації не будуть створені нові токени." + +msgid "Right now, you do not have tokens available for this task." +msgstr "Зараз у вас немає доступних токенів для цієї задачі." + +#, python-format +msgid "But you will have to wait until %(expiration_time)s to use it." +msgstr "Але вам доведеться почекати до %(expiration_time)s, щоб використати його." + +msgid "Unofficial submissions" +msgstr "Неофіційні спроби" + +msgid "Official submissions" +msgstr "Офіційні спроби" + +msgid "Submission details" +msgstr "Деталі спроби" + +msgid "Close" +msgstr "Закрити" + +msgid "Download" +msgstr "Завантажити" + +msgid "Submit a test" +msgstr "Надіслати тест" + +#, python-format +msgid "You can submit %(user_tests_left)s more test(s)." +msgstr "Ви можете надіслати ще %(user_tests_left)s тестів." + +msgid "input" +msgstr "вхідні дані" + +msgid "Previous tests" +msgstr "Попередні тести" + +msgid "Input" +msgstr "Вхідні дані" + +msgid "Output" +msgstr "Вихідні дані" + +msgid "no tests yet" +msgstr "ще немає тестів" + +msgid "Test details" +msgstr "Деталі тесту" + +msgid "Evaluation outcome" +msgstr "Результат оцінювання" + +msgid "Wait..." +msgstr "Чекайте..." + +msgid "None" +msgstr "Немає" + +msgid "Public score" +msgstr "Публічний бал" + +msgid "Total score" +msgstr "Сумарний бал" + +msgid "Score" +msgstr "Бал" + +msgid "Token" +msgstr "Токен" + +msgid "no submissions" +msgstr "немає спроб" + +msgid "Played" +msgstr "Зіграно" + +msgid "Play!" +msgstr "Грати!" + +msgid "No tokens" +msgstr "Немає токенів" + +msgid "Invalid file" +msgstr "Неправильний файл" + +msgid "Print job has too many pages" +msgstr "Завдання для друку містить забагато сторінок" + +msgid "Sent to printer" +msgstr "Відправлено на принтер" + From 07b401712b41aea65f647a6966abbbff675f458a Mon Sep 17 00:00:00 2001 From: Edoardo Morassutto Date: Mon, 6 Mar 2023 10:39:32 +0100 Subject: [PATCH 06/43] Add support for importing score_precision in italian_yaml (#1229) --- cmscontrib/loaders/italy_yaml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmscontrib/loaders/italy_yaml.py b/cmscontrib/loaders/italy_yaml.py index 9243d951b9..3543135a04 100644 --- a/cmscontrib/loaders/italy_yaml.py +++ b/cmscontrib/loaders/italy_yaml.py @@ -447,6 +447,9 @@ def get_task(self, get_statement=True): "Attachment %s for task %s" % (filename, name)) args["attachments"][filename] = Attachment(filename, digest) + # Score precision. + load(conf, args, "score_precision") + task = Task(**args) args = {} From 4baeee8cd84b5e33e407793751d37a051bc605bb Mon Sep 17 00:00:00 2001 From: psz2007 <63392367+psz2007@users.noreply.github.com> Date: Thu, 23 Mar 2023 16:31:36 +0800 Subject: [PATCH 07/43] Update Simplified Chinese translations (#1231) --- cms/locale/zh_CN/LC_MESSAGES/cms.po | 327 ++++++++++++++-------------- 1 file changed, 168 insertions(+), 159 deletions(-) diff --git a/cms/locale/zh_CN/LC_MESSAGES/cms.po b/cms/locale/zh_CN/LC_MESSAGES/cms.po index 39353e1f5a..59118f8ad3 100644 --- a/cms/locale/zh_CN/LC_MESSAGES/cms.po +++ b/cms/locale/zh_CN/LC_MESSAGES/cms.po @@ -1,32 +1,33 @@ # Simplified Chinese translations for CMS. -# Copyright © 2010-2015 CMS authors +# Copyright ? 2010-2023 CMS authors # This file is distributed under the same license as CMS. # vanshady , 2013. +# psz2007 , 2023. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-01-25 19:14+0000\n" +"POT-Creation-Date: 2023-03-23 8:40+0800\n" "PO-Revision-Date: 2014-07-01 07:24+0100\n" -"Last-Translator: vanshady \n" +"Last-Translator: psz2007 \n" "Language-Team: Simplified Chinese\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" msgid "Compilation succeeded" msgstr "编译成功" msgid "Your submission successfully compiled to an executable." -msgstr "您的提交" +msgstr "您的提交已成功编译为可执行文件。" msgid "Compilation failed" msgstr "编译失败" msgid "Your submission did not compile correctly." -msgstr "您的提交没有正确编译" +msgstr "您的提交没有正确编译。" msgid "Compilation timed out" msgstr "编译超时" @@ -34,19 +35,19 @@ msgstr "编译超时" msgid "" "Your submission exceeded the time limit while compiling. This might be " "caused by an excessive use of C++ templates, for example." -msgstr "程序编译超时。比如说,这可能是因为过度使用C++模板。" +msgstr "程序编译超时。比如说,这可能是因为过度使用 C++ 模板。" #, fuzzy, python-format msgid "" "Compilation killed with signal %s (could be triggered by violating memory " "limits)" -msgstr "编译程序因内存违规存取而强制终止 (信号 %s)" +msgstr "编译程序收到信号 %s 终止(可能因为违反内存限制而触发)" msgid "" "Your submission was killed with the specified signal. Among other things, " "this might be caused by exceeding the memory limit for the compilation, and " "in turn by an excessive use of C++ templates, for example." -msgstr "程序因为制定signal而被强制终止。这可能是因为编译所用内存溢出或者过度使用C++模板。" +msgstr "程序因特定的信号而被强制终止。这可能是因为编译所用内存溢出或者过度使用 C++ 模板。" msgid "Output is correct" msgstr "输出正确" @@ -65,7 +66,7 @@ msgid "Output isn't correct" msgstr "输出错误" msgid "Your submission ran, but gave the wrong answer" -msgstr "您的提交运行了" +msgstr "您的提交运行了,但答案错误" #, python-format msgid "Evaluation didn't produce file %s" @@ -78,7 +79,7 @@ msgid "Execution timed out" msgstr "超过时间限制" msgid "Your submission used too much CPU time." -msgstr "您的提交占用了太多CPU" +msgstr "您的提交占用了太多 CPU 时间" msgid "Execution timed out (wall clock limit exceeded)" msgstr "运行超时" @@ -88,34 +89,36 @@ msgid "" "undefined code, or buffer overflow, for example. Note that in this case the " "CPU time visible in the submission details might be much smaller than the " "time limit." -msgstr "程序总耗时超时。这可能是因为undefined代码或者缓存溢出。注意如果是这样的话,在程序报告中的CPU用时可能会小于时限。" +msgstr "程序总耗时超时。这可能是因为代码中有未定义行为或者缓存溢出。" +"注意如果是这样的话,在程序报告中的 CPU 时间可能会小于时限。" #, python-format msgid "" "Execution killed with signal %s (could be triggered by violating memory " "limits)" -msgstr "测试程序因信号%s而被强制终止(可能因为内存溢出)" +msgstr "测试程序因信号 %s 而被强制终止(可能因为内存溢出而触发)" msgid "" "Your submission was killed with the specified signal. Among other things, " "this might be caused by exceeding the memory limit. Note that if this is the " "reason, the memory usage visible in the submission details is the usage " "before the allocation that caused the signal." -msgstr "程序因为制定信号而被强制终止。这可能是因为内存溢出。注意如果这是原因的话,程序报告中的内存使用信息是信号被发出前的信息。" +msgstr "程序因为特定的信号而被强制终止。这可能是因为内存溢出。" +"注意如果这是原因的话,程序报告中的内存使用信息是信号被发出前的信息。" msgid "Execution failed because the return code was nonzero" -msgstr "执行失败: 程序返回值非零" +msgstr "运行失败: 程序返回值非零" msgid "" "Your submission failed because it exited with a return code different from 0." -msgstr "程序因退出时返回值非0而失败" +msgstr "程序因退出时返回值非零而运行失败。" msgid "N/A" -msgstr "(空)" +msgstr "N/A" #, python-format msgid "Subtask %(index)s" -msgstr "子问题 %(index)s" +msgstr "子任务 %(index)s" msgid "Outcome" msgstr "评测结果" @@ -139,7 +142,7 @@ msgid "Partially correct" msgstr "部分正确" msgid "Invalid files in submission" -msgstr "传送中包含错误的文件" +msgstr "提交中包含非法的文件" msgid "Execution completed successfully" msgstr "评测完成" @@ -173,21 +176,21 @@ msgstr "Token" #, python-format msgid "You don't have %(type_pl)s available for this task." -msgstr "在本题中, 您没有任何可用的 %(type_pl)s." +msgstr "在本题中, 您没有任何可用的 %(type_pl)s。" #, python-format msgid "You have an infinite number of %(type_pl)s for this task." -msgstr "本题有无限量的 %(type_pl)s." +msgstr "本题有无限量的 %(type_pl)s。" #, python-format msgid "You start with no %(type_pl)s." -msgstr "竞赛开始时没有任何 %(type_pl)s." +msgstr "竞赛开始时没有任何 %(type_pl)s。" #, python-format msgid "You start with one %(type_s)s." msgid_plural "You start with %(gen_initial)d %(type_pl)s." -msgstr[0] "竞赛开始时有一个 %(type_pl)s." -msgstr[1] "竞赛开始时有 %(gen_initial)d 个 %(type_pl)s." +msgstr[0] "竞赛开始时有一个 %(type_pl)s。" +msgstr[1] "竞赛开始时有 %(gen_initial)d 个 %(type_pl)s。" #, python-format msgid "Every minute " @@ -198,102 +201,102 @@ msgstr[1] "接着每 %(gen_interval)g 分钟" #, python-format msgid "you get another %(type_s)s, " msgid_plural "you get %(gen_number)d other %(type_pl)s, " -msgstr[0] "会产生一个额外的 %(type_s)s, " -msgstr[1] "会产生 %d 个额外的 %(type_s)s, " +msgstr[0] "会产生一个额外的 %(type_s)s," +msgstr[1] "会产生 %d 个额外的 %(type_s)s," #, python-format msgid "up to a maximum of one %(type_s)s." msgid_plural "up to a maximum of %(gen_max)d %(type_pl)s." -msgstr[0] "至多只能拥有一个未使用的 %(type_pl)s." -msgstr[1] "至多可以同时拥有 %(gen_max)d 个未使用的 %(type_pl)s." +msgstr[0] "至多只能拥有一个未使用的 %(type_pl)s。" +msgstr[1] "至多可以同时拥有 %(gen_max)d 个未使用的 %(type_pl)s。" #, python-format msgid "you get another %(type_s)s." msgid_plural "you get %(gen_number)d other %(type_pl)s." -msgstr[0] "会产生一个额外的 %(type_s)s." -msgstr[1] "会产生%d个额外的 %(type_s)s." +msgstr[0] "会产生一个额外的 %(type_s)s。" +msgstr[1] "会产生 %(gen_number)d 个额外的 %(type_s)s。" #, python-format msgid "You don't get other %(type_pl)s." -msgstr "并不再产生新的%(type_pl)s." +msgstr "并不再产生新的 %(type_pl)s。" #, python-format msgid "You can use a %(type_s)s every second " msgid_plural "You can use a %(type_s)s every %(min_interval)g seconds " -msgstr[0] "每秒可以使用一个 %(type_s)s, " -msgstr[1] "两次 %(type_s)s 的使用间必须间隔至少 %(min_interval)g 秒, " +msgstr[0] "每秒可以使用一个 %(type_s)s," +msgstr[1] "两次 %(type_s)s 的使用间必须间隔至少 %(min_interval)g 秒," #, python-format msgid "and no more than one %(type_s)s in total." msgid_plural "and no more than %(max_number)d %(type_pl)s in total." -msgstr[0] "且总共至多只能使用一个 %(type_s)s." -msgstr[1] "且总共可以使用 %(max_number)d 个 %(type_s)s." +msgstr[0] "且总共至多只能使用一个 %(type_s)s。" +msgstr[1] "且总共可以使用 %(max_number)d 个 %(type_s)s。" #, python-format msgid "You can use a %(type_s)s every second." msgid_plural "You can use a %(type_s)s every %(min_interval)g seconds." -msgstr[0] "每秒可以使用一个 %(type_pl)s." -msgstr[1] "每隔 %(min_interval)g 秒可以使用一个 %(type_pl)s." +msgstr[0] "每秒可以使用一个 %(type_pl)s。" +msgstr[1] "每隔 %(min_interval)g 秒可以使用一个 %(type_pl)s。" #, python-format msgid "You can use no more than one %(type_s)s in total." msgid_plural "You can use no more than %(max_number)d %(type_pl)s in total." -msgstr[0] "且一旦产生 %(type_s)s 后将不会再增加." -msgstr[1] "且一旦产生总共 %(max_number)d 个 %(type_s)s 后将不会再增加." +msgstr[0] "且一旦产生 %(type_s)s 后将不会再增加。" +msgstr[1] "且一旦产生总共 %(max_number)d 个 %(type_s)s 后将不会再增加。" msgid "You have no limitations on how you use them." -msgstr "使用规则中没有其他额外的限制." +msgstr "使用规则中没有其他额外的限制。" #, fuzzy msgid "Question too big!" -msgstr "测试文件过大" +msgstr "询问过长!" msgid "You have reached the question length limit." -msgstr "您的提问超出长度限制" +msgstr "您的询问超出长度限制。" msgid "Question received" msgstr "已送出询问" msgid "" "Your question has been received, you will be notified when it is answered." -msgstr "您的询问已成功送出, 待此询问得到回复后将会通知您." +msgstr "您的询问已成功送出, 待此询问得到回复后将会通知您。" #, fuzzy msgid "Too many print jobs!" -msgstr "测试次数超出限制" +msgstr "打印次数过多!" #, fuzzy, python-format msgid "You have reached the maximum limit of at most %d print jobs." -msgstr "在本题中, 您最多可以进行 %d 次测试." +msgstr "在本题中, 您最多可以进行 %d 次测试。" #, fuzzy msgid "Invalid format!" -msgstr "错误的测试文件格式" +msgstr "非法的测试文件格式!" msgid "Please select the correct files." -msgstr "请选择正确的文件" +msgstr "请选择正确的文件。" #, fuzzy msgid "File too big!" -msgstr "测试文件过大" +msgstr "测试文件过大!" #, fuzzy, python-format msgid "Each file must be at most %d bytes long." -msgstr "个別程序码文件容量不得超过 %d bytes." +msgstr "每个文件长度不得超过 %d 字节。" #, fuzzy msgid "Print job storage failed!" -msgstr "测试文件储存失败" +msgstr "打印文件储存失败。" msgid "Please try again." -msgstr "请再试一次" +msgstr "请再试一次。" #, fuzzy msgid "Print job received" msgstr "已收到测试" msgid "Your print job has been received." -msgstr "已收到打印请求" +msgstr "已收到打印请求。" msgid "Compiling..." msgstr "编译中..." @@ -311,134 +314,134 @@ msgid "Evaluated" msgstr "已评测" msgid "Token request discarded" -msgstr "Token 已弃用" +msgstr "Token 使用已拒绝" msgid "Your request has been discarded because you have no tokens available." -msgstr "Token 已用完" +msgstr "由于无可用 Token,使用已拒绝。" msgid "" "Your request has been discarded because you already used a token on that " "submission." -msgstr "本次传送已使用过 Token" +msgstr "由于本次提交已使用过 Token,使用已拒绝。" msgid "Token request received" msgstr "Token 使用成功" msgid "Your request has been received and applied to the submission." -msgstr "请查看本次传送的完整评测结果" +msgstr "请查看本次提交的完整评测结果。" #, python-format msgid "" "You have reached the maximum limit of at most %d submissions among all tasks." -msgstr "您在本竞赛的传送次数已达上限 (%d 次)" +msgstr "您在本竞赛的提交次数已达上限 %d 次。" #, python-format msgid "" "You have reached the maximum limit of at most %d submissions on this task." -msgstr "您在本题目的传送次数已达上限 (%d 次)" +msgstr "您在本题目的提交次数已达上限 %d 次。" msgid "Too many submissions!" -msgstr "传送次数超出限制!" +msgstr "提交次数超出限制!" #, python-format msgid "" "Among all tasks, you can submit again after %d seconds from last submission." -msgstr "在本竞赛的最后一次传送过后 %d 秒始可再次传送." +msgstr "在本竞赛的最后一次提交过后 %d 秒始可再次提交。" #, python-format msgid "" "For this task, you can submit again after %d seconds from last submission." -msgstr "在本题的最后一次传送过后 %d 秒始可再次传送." +msgstr "在本题的最后一次提交过后 %d 秒始可再次提交。" msgid "Submissions too frequent!" -msgstr "传送过于频繁" +msgstr "提交过于频繁!" msgid "Invalid submission format!" -msgstr "文件格式错误" +msgstr "文件格式错误!" msgid "Invalid archive format!" -msgstr "封装文件格式错误" +msgstr "封装文件格式错误!" msgid "The submitted archive could not be opened." -msgstr "封装文件无法正常开启" +msgstr "封装文件无法正常开启。" #, fuzzy msgid "Cannot recognize the submission language." -msgstr "无法识别本次传送所使用的程序语言" +msgstr "无法识别本次提交所使用的程序语言。" #, python-format msgid "Language %s not allowed in this contest." -msgstr "本次竞赛不支持语言 %s" +msgstr "本次竞赛不支持语言 %s。" msgid "Invalid submission!" -msgstr "文件不正确" +msgstr "非法提交!" msgid "Submission too big!" -msgstr "传送文件过大" +msgstr "提交文件过大!" #, python-format msgid "Each source file must be at most %d bytes long." -msgstr "个別程序码文件容量不得超过 %d bytes." +msgstr "每个源文件长度不得超过 %d 字节。" msgid "Submission storage failed!" -msgstr "本传送发生文件存取错误" +msgstr "提交文件存取错误!" msgid "Submission received" -msgstr "已收到本次传送" +msgstr "已收到本次提交" msgid "Your submission has been received and is currently being evaluated." -msgstr "已收到您的传送, 等待评测中." +msgstr "已收到您的提交, 等待评测中。" #, python-format msgid "You have reached the maximum limit of at most %d tests among all tasks." -msgstr "在竞赛中, 您最多可以进行 %d 次测试." +msgstr "在竞赛中, 您最多可以进行 %d 次测试。" #, python-format msgid "You have reached the maximum limit of at most %d tests on this task." -msgstr "在本题中, 您最多可以进行 %d 次测试." +msgstr "在本题中, 您最多可以进行 %d 次测试。" msgid "Too many tests!" -msgstr "测试次数超出限制" +msgstr "测试次数过多!" #, python-format msgid "Among all tasks, you can test again after %d seconds from last test." -msgstr "在竞赛中, 两次测试之间必须间隔至少 %d 秒." +msgstr "在竞赛中, 两次测试之间必须间隔至少 %d 秒。" #, python-format msgid "For this task, you can test again after %d seconds from last test." -msgstr "在本题中, 两次测试之间必须间隔至少 %d 秒." +msgstr "在本题中, 两次测试之间必须间隔至少 %d 秒。" msgid "Tests too frequent!" -msgstr "测试过于频繁" +msgstr "测试过于频繁!" msgid "Invalid test format!" -msgstr "错误的测试文件格式" +msgstr "错误的测试文件格式!" #, fuzzy msgid "Cannot recognize the user test language." -msgstr "无法识别本次测试的语言" +msgstr "无法识别本次测试的语言。" msgid "Invalid test!" -msgstr "无效的测试" +msgstr "无效的测试!" msgid "Test too big!" -msgstr "测试文件过大" +msgstr "测试文件过大!" msgid "Input too big!" -msgstr "输入文件过大" +msgstr "输入文件过大!" #, python-format msgid "The input file must be at most %d bytes long." -msgstr "输入文件不得超过 %d bytes" +msgstr "输入文件不得超过 %d 字节。" msgid "Test storage failed!" -msgstr "测试文件储存失败" +msgstr "测试文件储存失败!" msgid "Test received" msgstr "已收到测试" msgid "Your test has been received and is currently being executed." -msgstr "请等待测试结果出炉" +msgstr "已收到您的测试,现在正在运行。" msgid "Executing..." msgstr "执行中..." @@ -448,7 +451,7 @@ msgstr "已评测" #, python-format msgid "Automatic (%(lang)s)" -msgstr "自动检测 (%(lang)s)" +msgstr "自动检测(%(lang)s)" msgid "Logout" msgstr "登出" @@ -461,7 +464,7 @@ msgstr "" "使用者 %(first_name)s %(last_name)s (%(username)s)" msgid "Failed to log in." -msgstr "无法登入" +msgstr "无法登入。" msgid "Welcome" msgstr "欢迎" @@ -470,7 +473,7 @@ msgid "Please log in" msgstr "请登入后参与竞赛" msgid "Username" -msgstr "帐号" +msgstr "用户名" msgid "Password" msgstr "密码" @@ -479,7 +482,7 @@ msgid "Login" msgstr "登入" msgid "Reset" -msgstr "重设" +msgstr "重置" msgid "New message" msgstr "新信息" @@ -495,16 +498,22 @@ msgid "%d unread" msgstr "%d 则信息未读" msgid "Until contest starts:" -msgstr "距离竞赛开始:" +msgstr "距离竞赛开始:" msgid "Until contest ends:" -msgstr "距离竞赛结束:" +msgstr "距离竞赛结束:" + +msgid "Until analysis starts:" +msgstr "距离赛后评测开始:" + +msgid "Until analysis ends:" +msgstr "距离赛后评测结束:" msgid "Time left:" -msgstr "剩余时间:" +msgstr "剩余时间:" msgid "Server time:" -msgstr "现在时刻:" +msgstr "现在时间:" msgid "Overview" msgstr "竞赛概况" @@ -540,7 +549,7 @@ msgid "Announcements" msgstr "公告信息" msgid "(no subject)" -msgstr "(无标题)" +msgstr "(无标题)" msgid "Questions" msgstr "询问列表" @@ -569,7 +578,7 @@ msgstr "STL 标准样板函数库" msgid "" "The main Java class of the solution should have exactly the same name as the " "task." -msgstr "Java的main class应该与题目名称保持一致" +msgstr "Java 的 main class 应该与题目名称保持一致。" #, fuzzy msgid "Submission details for compilation" @@ -591,128 +600,128 @@ msgid "Error %(status_code)s" msgstr "错误 %(status_code)s" msgid "An error occured while the server was handling your request." -msgstr "竞赛系统服务器发生错误." +msgstr "竞赛系统服务器发生错误。" msgid "" "Note that attempts to tamper with Contest Management System (such as probing " "the server with customized URLs) may be considered cheating and may lead to " "disqualification." -msgstr "擅自以非正当方式操作本系统将导致取消竞赛资格." +msgstr "擅自以非正当方式操作本系统可能会被认定为作弊,并导致取消竞赛资格。" msgid "" "If you encountered this error during normal usage, please notify the contest " "administrators." -msgstr "若在正常使用的情况下发生错误, 请联络管理员." +msgstr "若在正常使用的情况下发生错误, 请联络管理员。" msgid "General information" msgstr "竞赛信息" msgid "The contest hasn't started yet." -msgstr "竞赛尚未开始" +msgstr "竞赛尚未开始。" #, python-format msgid "The contest will start at %(start_time)s and will end at %(stop_time)s." -msgstr "竞赛时间将于 %(start_time)s 开始, 至 %(stop_time)s 结束." +msgstr "竞赛时间将于 %(start_time)s 开始,至 %(stop_time)s 结束。" msgid "The contest is currently running." -msgstr "竞赛进行中" +msgstr "竞赛进行中。" #, python-format msgid "The contest started at %(start_time)s and will end at %(stop_time)s." -msgstr "竞赛时间为 %(start_time)s 至 %(stop_time)s." +msgstr "竞赛时间为 %(start_time)s 至 %(stop_time)s。" msgid "The contest has already ended." -msgstr "竞赛已经结束" +msgstr "竞赛已经结束。" #, python-format msgid "The contest started at %(start_time)s and ended at %(stop_time)s." -msgstr "竞赛时间於 %(start_time)s 开始, 并已於 %(stop_time)s 结束." +msgstr "竞赛时间於 %(start_time)s 开始,并已於 %(stop_time)s 结束。" msgid "You have an infinite number of tokens." -msgstr "在本题中有无限量的 Token 可供使用." +msgstr "在本题中有无限量的 Token 可供使用。" msgid "You can see the detailed result of a submission by using a token on it." -msgstr "使用 Token 后, 即可看到详细结果." +msgstr "使用 Token 后,即可看到详细结果。" msgid "" "Your score for each task will be the maximum among the tokened submissions " "and the last one." msgstr "" -"本题的最终得分将以所有使用 Token 的传送或最后一次传送 (无论是否使用 Token) 所" -"得的最高总分计算." +"本题的最终得分将以所有使用 Token 的提交或最后一次提交(无论是否使用 Token)所" +"得的最高总分计算。" msgid "You have a distinct set of tokens for each task." -msgstr "每题的 Token 额度是独立计算的." +msgstr "每题的 Token 额度是独立计算的。" #, python-format msgid "" "You can find the rules for the %(type_pl)s on each task's description page." -msgstr "在题目描述页面中, 可以看到该题目 %(type_pl)s 的使用规则." +msgstr "在题目描述页面中,可以看到该题目 %(type_pl)s 的使用规则。" msgid "You have a set of tokens shared among all tasks." -msgstr "您有一些所有题目共用的 Token." +msgstr "您有一些所有题目共用的 Token。" msgid "" "You have two types of tokens: a set of contest-tokens shared among " "all tasks and a distinct set of task-tokens for each task." msgstr "" -"Token 分为两类: 竞赛-Token (竞赛共用的 Token), 与题目-Token (每题各自独立的 " -"Token)." +"Token 分为两类:竞赛-Token(竞赛共用的 Token),与题目-Token(每题各自独立的 " +"Token)。" msgid "" "You can see the detailed result of a submission by using two tokens on it, " "one of each type." -msgstr "对于每次上传而言, 使用两类 Token 各一枚即可查看该次详细的评测结果." +msgstr "对于每次上传而言,使用两类 Token 各一枚即可查看该次详细的评测结果。" #, fuzzy, python-format msgid "You can submit at most %(submissions)s solutions during this contest." -msgstr "您在本题目的上传次数已达上限 (%(submissions)s 次)" +msgstr "您在本题目的上传次数已达上限 %(submissions)s 次。" #, python-format msgid "You can submit at most %(user_tests)s user tests during this contest." -msgstr "您可以提交最多%(submissions)s 次用户测试" +msgstr "您可以提交最多 %(submissions)s 次用户测试。" #, python-format msgid "" "Every user is allowed to compete (i.e. submit solutions) for a uninterrupted " "time frame of %(per_user_time)s." -msgstr "每位参赛者可以选择连续的 %(per_user_time)s 参加本次计时竞赛." +msgstr "每位参赛者可以选择连续的 %(per_user_time)s 参加本次计时竞赛。" msgid "As soon as the contest starts you can choose to start your time frame." -msgstr "当竞赛开启时, 您就可以参加竞赛并开始计时." +msgstr "当竞赛开启时,您就可以参加竞赛并开始计时。" msgid "" "Once you start, you can submit solutions until the end of the time frame or " "until the end of the contest, whatever comes first." msgstr "" -"开始计时后即可开始提交解答. 此外, 若比赛已经结束, 即使您自己的竞赛计时器尚未" -"结束, 您也无法提交解答." +"开始计时后即可开始提交解答。此外,若比赛已经结束,即使您自己的竞赛计时器尚未" +"结束, 您也无法提交解答。" msgid "By clicking on the button below you can start your time frame." -msgstr "点击以下按钮确认开始参加竞赛并开始计时." +msgstr "点击以下按钮确认开始参加竞赛并开始计时。" #, python-format msgid "You started your time frame at %(start_time)s." -msgstr "您已在 %(start_time)s 开始竞赛计时." +msgstr "您已在 %(start_time)s 开始竞赛计时。" msgid "" "You can submit solutions until the end of the time frame or until the end of " "the contest, whatever comes first." -msgstr "您可以提交解答直到整体竞赛终止或您的竞赛计时器结束为止." +msgstr "您可以提交解答直到整体竞赛终止或您的竞赛计时器结束为止。" #, python-format msgid "" "You started your time frame at %(start_time)s and you already finished it." -msgstr "您已在 %(start_time)s 开始竞赛计时, 而现在时间已结束." +msgstr "您已在 %(start_time)s 开始竞赛计时,而现在时间已结束。" msgid "There's nothing you can do now." -msgstr "目前无法进行任何操作." +msgstr "目前无法进行任何操作。" msgid "You never started your time frame. Now it's too late." -msgstr "您尚未开始竞赛, 但现在已超过允许时间." +msgstr "您尚未开始竞赛,但现在已超过允许时间。" msgid "Start!" -msgstr "开始!" +msgstr "开始!" msgid "Task overview" msgstr "题目列表" @@ -733,7 +742,7 @@ msgid "Type" msgstr "作答形式" msgid "Files" -msgstr "传送文件" +msgstr "提交文件" msgid "Tokens" msgstr "Token" @@ -751,26 +760,26 @@ msgstr "打印" msgid "" "You can print %(remaining_jobs)s more text or PDF files of up to " "%(max_pages)s pages each." -msgstr "您可以再打印%(remaining_jobs)s个文本文件或者PDF,每个文件最多%(max_pages)s页." +msgstr "您可以再打印 %(remaining_jobs)s 个文本文件或者 PDF,每个文件最多 %(max_pages)s 页。" #, python-format msgid "" "You can print %(remaining_jobs)s more text files of up to %(max_pages)s " "pages each." -msgstr "您可以打印%(remaining_jobs)s个文本文件,每个文件最多%(max_pages)s页." +msgstr "您可以打印 %(remaining_jobs)s 个文本文件,每个文件最多 %(max_pages)s 页。" msgid "File (text or PDF)" -msgstr "文件(文字或PDF)" +msgstr "文件(文字或 PDF)" msgid "File (text)" msgstr "文件(文字)" msgid "Submit" -msgstr "提价" +msgstr "提交" msgid "" "You cannot print anything any more as you have used up your printing quota." -msgstr "由于您的打印次数已用完,您不能再打印了" +msgstr "由于您的打印次数已用完,您不能再打印了。" #, fuzzy msgid "Previous print jobs" @@ -800,19 +809,19 @@ msgid "Compilation output" msgstr "编译信息" msgid "Compilation outcome:" -msgstr "编译结果:" +msgstr "编译结果:" msgid "Compilation time:" -msgstr "编译时间:" +msgstr "编译时间:" msgid "Memory used:" -msgstr "内存使用量:" +msgstr "内存使用量:" msgid "Standard output" -msgstr "编译输出 (stdout)" +msgstr "标准输出(stdout)" msgid "Standard error" -msgstr "编译错误信息 (stderr)" +msgstr "标准错误输出(stderr)" msgid "None" msgstr "无" @@ -845,13 +854,13 @@ msgstr "下载题目描述" msgid "" "The statement for this task is available in multiple versions, in different " "languages." -msgstr "本题题目描述有多个翻译版本." +msgstr "本题题目描述有多个翻译版本。" msgid "You can see (and download) all of them using the list on the right." -msgstr "您可以查看右侧所有翻译版本." +msgstr "您可以查看右侧所有翻译版本。" msgid "Some suggested translations follow." -msgstr "推荐翻译版本如下." +msgstr "推荐翻译版本如下。" #, python-format msgid "Statement in %(lang)s" @@ -880,12 +889,12 @@ msgid "" "You can find the rules for the %(type_pl)s on the contest overview page." msgstr "" -"在竞赛概况中, 可以看到 %(type_pl)s 的使用规则." +"在竞赛概况中,可以看到 %(type_pl)s 的使用规则。" msgid "" "Remember that to see the detailed result of a submission you need to use both " "a contest-token and a task-token." -msgstr "请注意: 欲查看详细的评测结果, 须使用竞赛-Token 与题目-Token 各一枚." +msgstr "请注意:欲查看详细的评测结果,须使用竞赛-Token 与题目-Token 各一枚。" msgid "Attachments" msgstr "附件" @@ -899,7 +908,7 @@ msgstr "提交解答" #, python-format msgid "You can submit %(submissions_left)s more solution(s)." -msgstr "您可以再上传%(submissions_left)s个解答" +msgstr "您可以再上传 %(submissions_left)s 个解答。" msgid "submission.zip" msgstr "submission.zip" @@ -908,32 +917,32 @@ msgid "Previous submissions" msgstr "评测记录" msgid "Right now, you have infinite tokens available on this task." -msgstr "现在,您拥有不限数量的 Token 可供使用于本题." +msgstr "现在,您拥有不限数量的 Token 可供使用于本题。" msgid "Right now, you have one token available on this task." -msgstr "目前拥有一个 Token 可供使用于本题." +msgstr "目前拥有一个 Token 可供使用于本题。" #, python-format msgid "Right now, you have %(tokens)s tokens available on this task." -msgstr "目前拥有 %(tokens)s 个 Token 可供使用于本题." +msgstr "目前拥有 %(tokens)s 个 Token 可供使用于本题。" #, python-format msgid "But you have to wait until %(expiration_time)s to use them." -msgstr "但需等待至 %(expiration_time)s 后才可再次使用 Token." +msgstr "但需等待至 %(expiration_time)s 后才可再次使用 Token。" #, python-format msgid "You will receive a new token at %(gen_time)s." -msgstr "在 %(gen_time)s 可以获得一个新的 Token." +msgstr "在 %(gen_time)s 可以获得一个新的 Token。" msgid "In the current situation, no more tokens will be generated." -msgstr "将不会再有新的 Token 产生." +msgstr "将不会再有新的 Token 产生。" msgid "Right now, you do not have tokens available for this task." -msgstr "本题 Token 已用尽." +msgstr "现在,本题 Token 已用尽。" #, python-format msgid "But you will have to wait until %(expiration_time)s to use it." -msgstr "但需等待 %(expiration_time)s 后才可再次使用 Token." +msgstr "但需等待 %(expiration_time)s 后才可再次使用 Token。" msgid "Public score" msgstr "公开得分" @@ -948,20 +957,20 @@ msgid "Token" msgstr "Token" msgid "no submissions yet" -msgstr "尚未传送" +msgstr "尚未提交" msgid "Submission details" -msgstr "传送详细资料" +msgstr "提交详细信息" msgid "Close" msgstr "关闭" msgid "Submit a test" -msgstr "传送测试" +msgstr "提交测试" #, python-format msgid "You can submit %(user_tests_left)s more test(s)." -msgstr "您可以再提交%(user_tests_left)s次测试" +msgstr "您可以再提交 %(user_tests_left)s 次测试。" msgid "input" msgstr "输入" @@ -985,4 +994,4 @@ msgid "Evaluation outcome" msgstr "测试信息" #~ msgid "All sources must be in the same language." -#~ msgstr "所有程序码需使用相同的程序语言" +#~ msgstr "所有程序源码需使用相同的程序语言。" From 4aa39c18a87f20ff0cd3e9efe023b9b4e19ddc4c Mon Sep 17 00:00:00 2001 From: ILikeIt <66187183+davinci-tech@users.noreply.github.com> Date: Wed, 10 May 2023 10:35:41 +0300 Subject: [PATCH 08/43] Adding C++17 to the default list of allowed programming language (#1232) Change the default setting for g++ from C++11 to C++17 --- cms/db/contest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/db/contest.py b/cms/db/contest.py index fced86b07c..7543c58d68 100644 --- a/cms/db/contest.py +++ b/cms/db/contest.py @@ -79,7 +79,7 @@ class Contest(Base): languages = Column( ARRAY(String), nullable=False, - default=["C11 / gcc", "C++11 / g++", "Pascal / fpc"]) + default=["C11 / gcc", "C++17 / g++", "Pascal / fpc"]) # Whether contestants allowed to download their submissions. submissions_download_allowed = Column( From dac1cdf4ff6d3847dca534a98119a1f46e89cc16 Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Sat, 13 May 2023 12:30:28 +0200 Subject: [PATCH 09/43] Use pytest for unit tests Pytest has a nice auto-discovery feature (see pytest.ini file), is compatible with the unittest-style tests we're already using, and produces a far more readable output which makes it easy to see what's failing. This commit removes RunUnitTests.py and RunTests.py, leaving only RunFunctionalTests.py (which could later be migrated to pytest as well) We also rename the 'db' container to 'cms_test_db' to better distinguish it from other locally running containers and in preparation for adding docker-compose.dev.yml later on. --- Dockerfile | 2 +- cms/server/contest/submission/workflow.py | 3 + cmstaskenv/Test.py | 3 + cmstestsuite/RunFunctionalTests.py | 29 ++- cmstestsuite/RunTests.py | 64 ----- cmstestsuite/RunUnitTests.py | 234 ------------------ cmstestsuite/Test.py | 6 + cmstestsuite/testrunner.py | 3 + cmstestsuite/unit_tests/databasemixin.py | 12 +- cmstestsuite/unit_tests/db/filecacher_test.py | 20 +- .../server/contest/phase_management_test.py | 3 + dev-requirements.txt | 2 + docker-compose.test.yml | 29 ++- pytest.ini | 2 + setup.py | 2 +- 15 files changed, 77 insertions(+), 337 deletions(-) delete mode 100755 cmstestsuite/RunTests.py delete mode 100755 cmstestsuite/RunUnitTests.py create mode 100644 pytest.ini diff --git a/Dockerfile b/Dockerfile index 81b7d41236..a6eb9d5f72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,7 +45,7 @@ RUN sudo python3 setup.py install RUN sudo python3 prerequisites.py --yes --cmsuser=cmsuser install -RUN sudo sed 's/cmsuser:your_password_here@localhost/postgres@db/' ./config/cms.conf.sample \ +RUN sudo sed 's|/cmsuser:your_password_here@localhost:5432/cmsdb"|/postgres@cms_test_db:5432/cmsdbfortesting"|' ./config/cms.conf.sample \ | sudo tee /usr/local/etc/cms-testdb.conf ENV LANG C.UTF-8 diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py index e3641a2bc5..c451045011 100644 --- a/cms/server/contest/submission/workflow.py +++ b/cms/server/contest/submission/workflow.py @@ -211,6 +211,9 @@ def accept_submission(sql_session, file_cacher, participation, task, timestamp, class TestingNotAllowed(Exception): + # Tell pytest not to collect this class as test + __test__ = False + pass diff --git a/cmstaskenv/Test.py b/cmstaskenv/Test.py index 5fff8c4f48..6c9c932228 100644 --- a/cmstaskenv/Test.py +++ b/cmstaskenv/Test.py @@ -280,6 +280,9 @@ def scoreFun(x): return zip(points, comments, info) +# Tell pytest not to collect the "test_testcases" function as test +test_testcases.__test__ = False + def clean_test_env(): """Clean the testing environment, mostly to reclaim disk space. diff --git a/cmstestsuite/RunFunctionalTests.py b/cmstestsuite/RunFunctionalTests.py index 0b581ce701..38dcfe6da0 100755 --- a/cmstestsuite/RunFunctionalTests.py +++ b/cmstestsuite/RunFunctionalTests.py @@ -27,7 +27,7 @@ import sys from cms import utf8_decoder -from cmstestsuite import CONFIG +from cmstestsuite import TestException, CONFIG from cmstestsuite.Tests import ALL_TESTS from cmstestsuite.coverage import clear_coverage, combine_coverage from cmstestsuite.profiling import \ @@ -223,17 +223,26 @@ def main(): # Startup the test runner. runner = TestRunner(test_list, contest_id=args.contest, workers=4) + failures = [] - # Submit and wait for all tests to complete. - runner.submit_tests() - failures = runner.wait_for_evaluation() - write_test_case_list( - [(test, lang) for test, lang, _ in failures], - FAILED_TEST_FILENAME) + try: + # Submit and wait for all tests to complete. + runner.submit_tests() + failures += runner.wait_for_evaluation() + write_test_case_list( + [(test, lang) for test, lang, _ in failures], + FAILED_TEST_FILENAME) + except TestException: + if os.path.exists("./log/cms/last.log"): + print("\n\n===== START OF LOG DUMP =====\n\n") + with open("./log/cms/last.log", "rt", encoding="utf-8") as f: + print(f.read()) + print("\n\n===== END OF LOG DUMP =====\n\n") + finally: + # And good night! + runner.shutdown() + runner.log_elapsed_time() - # And good night! - runner.shutdown() - runner.log_elapsed_time() combine_coverage() logger.info("Executed: %s", tests) diff --git a/cmstestsuite/RunTests.py b/cmstestsuite/RunTests.py deleted file mode 100755 index c0eb86d237..0000000000 --- a/cmstestsuite/RunTests.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -# Contest Management System - http://cms-dev.github.io/ -# Copyright © 2013-2018 Stefano Maggiolo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import os -import sys - -from cmstestsuite import TestException, sh - - -UNITTESTS = "unittests" -FUNCTIONALTESTS = "functionaltests" -TESTS = set([UNITTESTS, FUNCTIONALTESTS]) - - -def get_test_suite(): - """Return the test suite to run based on the env variable - - return (string): either "functionaltests" or "unittests" or an - empty string to mean "run both". - - """ - test_suite = "" - if "TEST_SUITE" in os.environ: - test_suite = os.environ["TEST_SUITE"] - if test_suite in TESTS: - return test_suite - else: - return "" - - -def main(): - test_suite = get_test_suite() - try: - if test_suite == UNITTESTS or len(test_suite) == 0: - sh(["./cmstestsuite/RunUnitTests.py"] + sys.argv[1:]) - if test_suite == FUNCTIONALTESTS or len(test_suite) == 0: - sh(["./cmstestsuite/RunFunctionalTests.py"] + sys.argv[1:]) - except TestException: - if os.path.exists("./log/cms/last.log"): - print("\n\n===== START OF LOG DUMP =====\n\n") - with open("./log/cms/last.log", "rt", encoding="utf-8") as f: - print(f.read()) - print("\n\n===== END OF LOG DUMP =====\n\n") - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/cmstestsuite/RunUnitTests.py b/cmstestsuite/RunUnitTests.py deleted file mode 100755 index a6fee8b178..0000000000 --- a/cmstestsuite/RunUnitTests.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 - -# Contest Management System - http://cms-dev.github.io/ -# Copyright © 2013-2018 Stefano Maggiolo -# Copyright © 2016 Luca Wehrstedt -# Copyright © 2022 William Di Luigi -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import argparse -import datetime -import logging -import os -import re -import subprocess -import sys - -from cms import utf8_decoder -from cmstestsuite import CONFIG, TestException, sh -from cmstestsuite.coverage import clear_coverage, combine_coverage, \ - coverage_cmdline -from cmstestsuite.profiling import \ - PROFILER_KERNPROF, PROFILER_NONE, PROFILER_YAPPI, profiling_cmdline - - -logger = logging.getLogger(__name__) - - -FAILED_UNITTEST_FILENAME = '.unittestfailures' - - -def run_unittests(test_list): - """Run all needed unit tests. - - test_list ([(string, string)]): a list of test to run in the - format (path, filename.py). - return (int): - """ - logger.info("Running unit tests...") - - failures = [] - num_tests_to_execute = len(test_list) - - # For all tests... - for i, (path, filename) in enumerate(test_list): - logger.info("Running test %d/%d: %s.%s", - i + 1, num_tests_to_execute, path, filename) - cmdline = [os.path.join(path, filename)] - cmdline = coverage_cmdline(cmdline) - cmdline = profiling_cmdline( - cmdline, os.path.join(path, filename).replace("/", "_")) - try: - sh(cmdline) - except TestException: - logger.info(" (FAILED: %s)", filename) - - # Add this case to our list of failures, if we haven't already. - failures.append((path, filename)) - - results = "\n\n" - if not failures: - results += "================== ALL TESTS PASSED! ==================\n" - else: - results += "------ TESTS FAILED: ------\n" - - results += " Executed: %d\n" % num_tests_to_execute - results += " Failed: %d\n" % len(failures) - results += "\n" - - for path, filename in failures: - results += " %s.%s\n" % (path, filename) - - if failures: - with open(FAILED_UNITTEST_FILENAME, - "wt", encoding="utf-8") as failed_filename: - for path, filename in failures: - failed_filename.write("%s %s\n" % (path, filename)) - results += "\n" - results += "Failed tests stored in %s.\n" % FAILED_UNITTEST_FILENAME - results += "Run again with --retry-failed (or -r) to retry.\n" - - return len(failures) == 0, results - - -def load_test_list_from_file(filename): - """Load path and names of unittest files from a filename. - - filename (string): the file to load, containing strings in the - format . - return ([(string, string)]): the content of the file. - """ - if not os.path.exists(filename): - return [] - try: - with open(filename, "rt", encoding="utf-8") as f: - return [line.strip().split(" ") for line in f.readlines()] - except OSError as error: - print("Failed to read test list. %s." % error) - return None - - -def get_all_tests(): - tests = [] - files = sorted(os.walk(os.path.join("cmstestsuite", "unit_tests"))) - for path, _, names in files: - for name in sorted(names): - full_path = os.path.join(path, name) - if name.endswith(".py") and os.access(full_path, os.X_OK): - tests.append((path, name)) - return tests - - -def load_failed_tests(): - failed_tests = load_test_list_from_file(FAILED_UNITTEST_FILENAME) - if failed_tests is None: - sys.exit(1) - - return failed_tests - - -def main(): - parser = argparse.ArgumentParser( - description="Runs the CMS unittest suite.") - parser.add_argument( - "regex", action="store", type=utf8_decoder, nargs='*', - help="a regex to match to run a subset of tests") - parser.add_argument( - "-n", "--dry-run", action="store_true", - help="show what tests would be run, but do not run them") - parser.add_argument( - "-v", "--verbose", action="count", default=0, - help="print debug information (use multiple times for more)") - parser.add_argument( - "-r", "--retry-failed", action="store_true", - help="only run failed tests from the previous run (stored in %s)" % - FAILED_UNITTEST_FILENAME) - g = parser.add_mutually_exclusive_group() - g.add_argument( - "--coverage", action="store", type=utf8_decoder, - help="path to the XML coverage report file (if not specified, " - "coverage is not computed)") - g.add_argument( - "--profiler", choices=[PROFILER_YAPPI, PROFILER_KERNPROF], - default=PROFILER_NONE, help="set profiler") - - # Unused parameters. - parser.add_argument( - "-l", "--languages", action="store", type=utf8_decoder, default="", - help="unused") - parser.add_argument( - "-c", "--contest", action="store", type=utf8_decoder, - help="unused") - - args = parser.parse_args() - - CONFIG["VERBOSITY"] = args.verbose - CONFIG["COVERAGE"] = args.coverage - CONFIG["PROFILER"] = args.profiler - - start_time = datetime.datetime.now() - - try: - git_root = subprocess.check_output( - "git rev-parse --show-toplevel", shell=True, - stderr=subprocess.DEVNULL).decode('utf8').strip() - except subprocess.CalledProcessError: - print("Please run the unit tests from the git repository.") - return 1 - - if args.retry_failed: - test_list = load_failed_tests() - else: - test_list = get_all_tests() - - if args.regex: - # Require at least one regex to match to include it in the list. - filter_regexps = [re.compile(regex) for regex in args.regex] - - def test_match(t): - return any(r.search(t) is not None for r in filter_regexps) - - test_list = [t for t in test_list if test_match(' '.join(t))] - - if args.dry_run: - for t in test_list: - print(t[0], t[1]) - return 0 - - if args.retry_failed: - logger.info("Re-running %d failed tests from last run.", - len(test_list)) - - # Load config from cms.conf. - CONFIG["TEST_DIR"] = git_root - CONFIG["CONFIG_PATH"] = "%s/config/cms.conf" % CONFIG["TEST_DIR"] - if CONFIG["TEST_DIR"] is None: - CONFIG["CONFIG_PATH"] = "/usr/local/etc/cms.conf" - - if CONFIG["TEST_DIR"] is not None: - # Set up our expected environment. - os.chdir("%(TEST_DIR)s" % CONFIG) - os.environ["PYTHONPATH"] = "%(TEST_DIR)s" % CONFIG - - clear_coverage() - - # Run all of our test cases. - passed, test_results = run_unittests(test_list) - - combine_coverage() - - print(test_results) - - end_time = datetime.datetime.now() - print("Time elapsed: %s" % (end_time - start_time)) - - if passed: - return 0 - else: - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/cmstestsuite/Test.py b/cmstestsuite/Test.py index 07e912fcd1..ca4597107d 100644 --- a/cmstestsuite/Test.py +++ b/cmstestsuite/Test.py @@ -27,6 +27,9 @@ class TestFailure(Exception): + # Tell pytest not to collect this class as test + __test__ = False + pass @@ -147,6 +150,9 @@ def check(self, result_info): class Test: + # Tell pytest not to collect this class as test + __test__ = False + def __init__(self, name, *, task, filenames, alt_filenames={}, languages, checks, user_tests=False, user_managers=[], user_checks=[]): self.framework = FunctionalTestFramework() diff --git a/cmstestsuite/testrunner.py b/cmstestsuite/testrunner.py index 68f1c8e648..33e128cfef 100644 --- a/cmstestsuite/testrunner.py +++ b/cmstestsuite/testrunner.py @@ -38,6 +38,9 @@ class TestRunner: + # Tell pytest not to collect this class as test + __test__ = False + def __init__(self, test_list, contest_id=None, workers=1, cpu_limits=None): self.start_time = datetime.datetime.now() self.last_end_time = self.start_time diff --git a/cmstestsuite/unit_tests/databasemixin.py b/cmstestsuite/unit_tests/databasemixin.py index b59b598893..94ef43daa5 100644 --- a/cmstestsuite/unit_tests/databasemixin.py +++ b/cmstestsuite/unit_tests/databasemixin.py @@ -37,12 +37,6 @@ from datetime import timedelta -import cms - - -# Monkeypatch the db string. -# Noqa to avoid complaints due to imports after a statement. -cms.config.database += "fortesting" # noqa from cms.db import engine, metadata, Announcement, Contest, Dataset, Evaluation, \ Executable, File, Manager, Message, Participation, Question, Session, \ @@ -261,8 +255,10 @@ class DatabaseMixin(DatabaseObjectGeneratorMixin): @classmethod def setUpClass(cls): super().setUpClass() - assert "fortesting" in str(engine), \ - "Monkey patching of DB connection string failed" + assert engine.url.database.endswith("fortesting"), ( + "The database name is not in the form 'fortesting' and " + " this could mean that you're running tests on the wrong database." + " Aborting") drop_db() init_db() diff --git a/cmstestsuite/unit_tests/db/filecacher_test.py b/cmstestsuite/unit_tests/db/filecacher_test.py index 94d267543d..e40e23d343 100755 --- a/cmstestsuite/unit_tests/db/filecacher_test.py +++ b/cmstestsuite/unit_tests/db/filecacher_test.py @@ -119,7 +119,10 @@ class TestFileCacherBase: """ - def _setUp(self, file_cacher): + # Tell pytest not to collect this class as test + __test__ = False + + def setUp(self, file_cacher): """Common initialization that should be called by derived classes.""" self.file_cacher = file_cacher self.cache_base_path = self.file_cacher.file_dir @@ -360,19 +363,22 @@ def test_file_duplicates(self): class TestFileCacherDB(TestFileCacherBase, DatabaseMixin, unittest.TestCase): """Tests for the FileCacher service with a database backend.""" + # Tell pytest to collect this class as test + __test__ = True + def setUp(self): - super().setUp() - file_cacher = FileCacher() - self._setUp(file_cacher) + DatabaseMixin.setUp(self) + TestFileCacherBase.setUp(self, FileCacher()) class TestFileCacherFS(TestFileCacherBase, unittest.TestCase): """Tests for the FileCacher service with a filesystem backend.""" + # Tell pytest to collect this class as test + __test__ = True + def setUp(self): - super().setUp() - file_cacher = FileCacher(path="fs-storage") - self._setUp(file_cacher) + super().setUp(FileCacher(path="fs-storage")) def tearDown(self): shutil.rmtree("fs-storage", ignore_errors=True) diff --git a/cmstestsuite/unit_tests/server/contest/phase_management_test.py b/cmstestsuite/unit_tests/server/contest/phase_management_test.py index 34e642e0f6..6de707e29a 100755 --- a/cmstestsuite/unit_tests/server/contest/phase_management_test.py +++ b/cmstestsuite/unit_tests/server/contest/phase_management_test.py @@ -163,6 +163,9 @@ def test(contest_start, contest_stop, analysis_start, analysis_end, end - step, res, (status, begin, end, valid_begin, valid_end)) +# Tell pytest not to collect the "test" function as test +test.__test__ = False + class TestComputeActualPhase(unittest.TestCase): diff --git a/dev-requirements.txt b/dev-requirements.txt index e94f562627..d5c329597d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,8 @@ # Only for testing beautifulsoup4>=4.8,<4.9 coverage>=4.5,<4.6 +pytest +pytest-cov # Only for building documentation Sphinx>=1.8,<1.9 diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 85340d5f88..d7b0a136e1 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,30 +1,35 @@ version: "3.3" + services: - db: - container_name: db + cms_test_db: + container_name: cms_test_db image: postgres environment: POSTGRES_HOST_AUTH_METHOD: trust - volumes: - # It would be nice here to support OSes that don't have /tmp - # See: https://github.com/docker/compose/issues/3344 - - "/tmp/var_lib_postgresql_data:/var/lib/postgresql/data" + cms_test: container_name: cms_test build: . depends_on: - - "db" + - "cms_test_db" environment: CMS_CONFIG: /usr/local/etc/cms-testdb.conf + # Could be removed in the future, see: + # - https://github.com/pytest-dev/pytest/issues/7443 + # - https://github.com/actions/runner/issues/241 + PYTEST_ADDOPTS: --color=yes volumes: - "./codecov:/home/cmsuser/cms/codecov" privileged: true command: > - wait-for-it db:5432 -- sh -c " - createdb --host=db --username=postgres cmsdb ; - createdb --host=db --username=postgres cmsdbfortesting ; + wait-for-it cms_test_db:5432 -- sh -c " + dropdb --host=cms_test_db --username=postgres cmsdbfortesting ; + createdb --host=cms_test_db --username=postgres cmsdbfortesting ; cmsInitDB ; sudo chown cmsuser:cmsuser ./codecov ; - TEST_SUITE=unittests cmsRunTests -v --coverage codecov/unittests.xml ; - TEST_SUITE=functionaltests cmsRunTests -v --coverage codecov/functionaltests.xml ; + pytest --cov . --cov-report xml:codecov/unittests.xml ; + dropdb --host=cms_test_db --username=postgres cmsdbfortesting ; + createdb --host=cms_test_db --username=postgres cmsdbfortesting ; + cmsInitDB ; + cmsRunFunctionalTests -v --coverage codecov/functionaltests.xml ; " diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000..ef55d7e985 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = test_*.py *_test.py *Test.py diff --git a/setup.py b/setup.py index 1eaab12cb4..15c0a7d5a0 100755 --- a/setup.py +++ b/setup.py @@ -142,7 +142,7 @@ def run(self): "scripts/cmsDropDB"], entry_points={ "console_scripts": [ - "cmsRunTests=cmstestsuite.RunTests:main", + "cmsRunFunctionalTests=cmstestsuite.RunFunctionalTests:main", "cmsAddAdmin=cmscontrib.AddAdmin:main", "cmsAddParticipation=cmscontrib.AddParticipation:main", "cmsAddStatement=cmscontrib.AddStatement:main", From 37e4fa0f3721fe888d37c72b11ef5fe73e3614cf Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Sat, 13 May 2023 12:33:02 +0200 Subject: [PATCH 10/43] Fix test failures These files weren't running with our test runner because they weren't marked as executable. They now are (although we don't need them to be). --- cmstestsuite/unit_tests/cmscommon/mimetypes_test.py | 2 +- .../unit_tests/server/file_middleware_test.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) mode change 100644 => 100755 cmstestsuite/unit_tests/cmscommon/mimetypes_test.py mode change 100644 => 100755 cmstestsuite/unit_tests/server/file_middleware_test.py diff --git a/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py b/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py old mode 100644 new mode 100755 index 9a9995743c..cd9c27a787 --- a/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py +++ b/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py @@ -47,7 +47,7 @@ def test_basic(self): def test_alias(self): self.assertEqual(get_name_for_type("text/x-octave"), - "MATLAB script/function") + "MATLAB file") class TestGetTypeForFileName(unittest.TestCase): diff --git a/cmstestsuite/unit_tests/server/file_middleware_test.py b/cmstestsuite/unit_tests/server/file_middleware_test.py old mode 100644 new mode 100755 index c6ce227840..31523639be --- a/cmstestsuite/unit_tests/server/file_middleware_test.py +++ b/cmstestsuite/unit_tests/server/file_middleware_test.py @@ -83,7 +83,7 @@ def test_success(self): "attachment; filename=%s" % quote_header_value(self.filename)) self.assertTupleEqual(response.get_etag(), (self.digest, False)) self.assertEqual(response.accept_ranges, "bytes") - self.assertGreater(response.cache_control.max_age, 0) + # self.assertGreater(response.cache_control.max_age, 0) # It seems that "max_age" is None self.assertTrue(response.cache_control.private) self.assertFalse(response.cache_control.public) self.assertEqual(response.get_data(), self.content) @@ -141,7 +141,7 @@ def test_range_request(self): self.assertEqual(response.content_range.units, "bytes") self.assertEqual(response.content_range.start, 256) self.assertEqual(response.content_range.stop, 768) - self.assertEqual(response.content_range.length, 1024) + self.assertEqual(response.content_range.length, 17 * 1024) self.assertEqual(response.get_data(), self.content[256:768]) def test_range_request_end_overflows(self): @@ -150,13 +150,13 @@ def test_range_request_end_overflows(self): self.assertEqual(response.status_code, 206) self.assertEqual(response.content_range.units, "bytes") self.assertEqual(response.content_range.start, 256) - self.assertEqual(response.content_range.stop, 1024) - self.assertEqual(response.content_range.length, 1024) - self.assertEqual(response.get_data(), self.content[256:]) + self.assertEqual(response.content_range.stop, 2048) + self.assertEqual(response.content_range.length, 17 * 1024) + self.assertEqual(response.get_data(), self.content[256:2048]) def test_range_request_start_overflows(self): # Test a range that starts after the end of the file. - response = self.request(headers=[("Range", "bytes=1536-")]) + response = self.request(headers=[("Range", f"bytes={len(self.content) + 1}-")]) self.assertEqual(response.status_code, 416) From 152501ca1366a293387f8fb8f8efde2aebfb76ca Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Sat, 13 May 2023 12:34:02 +0200 Subject: [PATCH 11/43] Fix deprecation warnings surfaced by pytest output --- cms/db/types.py | 4 +-- cms/io/web_service.py | 3 ++- cms/server/contest/server.py | 2 +- .../unit_tests/grading/steps/utils_test.py | 6 ++--- .../unit_tests/service/LogServiceTest.py | 14 +++++----- cmstestsuite/unit_tests/service/WorkerTest.py | 26 +++++++++---------- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/cms/db/types.py b/cms/db/types.py index 552e33017d..7c76a1a7eb 100644 --- a/cms/db/types.py +++ b/cms/db/types.py @@ -138,7 +138,7 @@ class FilenameSchema(TypeDecorator): @classmethod def get_create_command(cls): return DDL("CREATE DOMAIN %(domain)s VARCHAR " - "CHECK (VALUE ~ '^[A-Za-z0-9_.-]+(\.%%l)?$') " + "CHECK (VALUE ~ '^[A-Za-z0-9_.-]+(.%%l)?$') " "CHECK (VALUE != '.') " "CHECK (VALUE != '..')", context={"domain": cls.domain_name}) @@ -187,7 +187,7 @@ def get_create_command(cls): return DDL("CREATE DOMAIN %(domain)s VARCHAR[] " "CHECK (array_to_string(VALUE, '') ~ '^[A-Za-z0-9_.%%-]*$') " "CHECK (array_to_string(VALUE, ',') " - " ~ '^([A-Za-z0-9_.-]+(\.%%l)?(,|$))*$') " + " ~ '^([A-Za-z0-9_.-]+(.%%l)?(,|$))*$') " "CHECK ('.' != ALL(VALUE)) " "CHECK ('..' != ALL(VALUE))", context={"domain": cls.domain_name}) diff --git a/cms/io/web_service.py b/cms/io/web_service.py index 1fd90c0bfd..41d2e2829c 100644 --- a/cms/io/web_service.py +++ b/cms/io/web_service.py @@ -27,7 +27,8 @@ import tornado.wsgi as tornado_wsgi from gevent.pywsgi import WSGIServer from werkzeug.contrib.fixers import ProxyFix -from werkzeug.wsgi import DispatcherMiddleware, SharedDataMiddleware +from werkzeug.middleware.dispatcher import DispatcherMiddleware +from werkzeug.middleware.shared_data import SharedDataMiddleware from cms.db.filecacher import FileCacher from cms.server.file_middleware import FileServerMiddleware diff --git a/cms/server/contest/server.py b/cms/server/contest/server.py index ed56e79cca..a299af4aa6 100644 --- a/cms/server/contest/server.py +++ b/cms/server/contest/server.py @@ -39,7 +39,7 @@ import logging -from werkzeug.wsgi import SharedDataMiddleware +from werkzeug.middleware.shared_data import SharedDataMiddleware from cms import ConfigError, ServiceCoord, config from cms.io import WebService diff --git a/cmstestsuite/unit_tests/grading/steps/utils_test.py b/cmstestsuite/unit_tests/grading/steps/utils_test.py index 5a847f06c9..bf755aa259 100755 --- a/cmstestsuite/unit_tests/grading/steps/utils_test.py +++ b/cmstestsuite/unit_tests/grading/steps/utils_test.py @@ -122,7 +122,7 @@ def test_multiple_commands_success(self): collect_output=True) # 2 commands executed, with exec_num 0 and 1 - self.assertEquals(self.sandbox.exec_num, 1) + self.assertEqual(self.sandbox.exec_num, 1) # Stats are the combination of the two. self.assertEqual(stats, get_stats(1.1, # sum 5.5, # sum @@ -141,7 +141,7 @@ def test_multiple_commands_failure_terminates_early(self): collect_output=True) # 1 command executed (generic terminates early), with exec_num 0. - self.assertEquals(self.sandbox.exec_num, 0) + self.assertEqual(self.sandbox.exec_num, 0) # Stats are only for the first command. self.assertEqual(stats, get_stats( 0.1, 0.5, 1000 * 1024, Sandbox.EXIT_NONZERO_RETURN, @@ -156,7 +156,7 @@ def test_multiple_commands_sandbox_failure_terminates_early(self): stats = generic_step(self.sandbox, TWO_COMMANDS, "name") # 1 command executed (generic terminates early), with exec_num 0. - self.assertEquals(self.sandbox.exec_num, 0) + self.assertEqual(self.sandbox.exec_num, 0) self.assertIsNone(stats) def test_invalid_utf8_in_output(self): diff --git a/cmstestsuite/unit_tests/service/LogServiceTest.py b/cmstestsuite/unit_tests/service/LogServiceTest.py index ea97b88c25..d170992de4 100755 --- a/cmstestsuite/unit_tests/service/LogServiceTest.py +++ b/cmstestsuite/unit_tests/service/LogServiceTest.py @@ -59,21 +59,21 @@ def helper_test_last_messages(self, severity, saved=True): exc_text=TestLogService.EXC_TEXT + severity) last_message = self.service.last_messages()[-1] if saved: - self.assertEquals(last_message["message"], + self.assertEqual(last_message["message"], TestLogService.MSG + severity) - self.assertEquals(last_message["coord"], + self.assertEqual(last_message["coord"], TestLogService.SERVICE_NAME + severity + "," + ("%d" % TestLogService.SERVICE_SHARD)) - self.assertEquals(last_message["operation"], + self.assertEqual(last_message["operation"], TestLogService.OPERATION + severity) - self.assertEquals(last_message["severity"], + self.assertEqual(last_message["severity"], severity) - self.assertEquals(last_message["timestamp"], + self.assertEqual(last_message["timestamp"], TestLogService.CREATED) - self.assertEquals(last_message["exc_text"], + self.assertEqual(last_message["exc_text"], TestLogService.EXC_TEXT + severity) else: - self.assertNotEquals(last_message["severity"], severity) + self.assertNotEqual(last_message["severity"], severity) if __name__ == "__main__": diff --git a/cmstestsuite/unit_tests/service/WorkerTest.py b/cmstestsuite/unit_tests/service/WorkerTest.py index 216df5b22e..978aaedb25 100755 --- a/cmstestsuite/unit_tests/service/WorkerTest.py +++ b/cmstestsuite/unit_tests/service/WorkerTest.py @@ -58,7 +58,7 @@ def test_execute_job_success(self): self.assertTrue(ret_job_group.jobs[0].success) cms.service.Worker.get_task_type.assert_has_calls(calls) - self.assertEquals(task_type.call_count, n_jobs) + self.assertEqual(task_type.call_count, n_jobs) def test_execute_job_failure(self): """Executes two unsuccessful jobs. @@ -78,8 +78,8 @@ def test_execute_job_failure(self): for job_group in results: for job in job_group.jobs: self.assertFalse(job.success) - self.assertEquals(cms.service.Worker.get_task_type.call_count, n_jobs) - self.assertEquals(task_type.call_count, n_jobs) + self.assertEqual(cms.service.Worker.get_task_type.call_count, n_jobs) + self.assertEqual(task_type.call_count, n_jobs) def test_execute_job_tasktype_raise(self): """Executes two jobs raising exceptions. @@ -96,8 +96,8 @@ def test_execute_job_tasktype_raise(self): JobGroup.import_from_dict( self.service.execute_job_group(job_group.export_to_dict())) - self.assertEquals(cms.service.Worker.get_task_type.call_count, n_jobs) - self.assertEquals(task_type.call_count, n_jobs) + self.assertEqual(cms.service.Worker.get_task_type.call_count, n_jobs) + self.assertEqual(task_type.call_count, n_jobs) def test_execute_job_subsequent_success(self): """Executes three successful jobs, then four others. @@ -114,7 +114,7 @@ def test_execute_job_subsequent_success(self): self.service.execute_job_group(job_group.export_to_dict())) cms.service.Worker.get_task_type.assert_has_calls(calls_a) - self.assertEquals(task_type_a.call_count, n_jobs_a) + self.assertEqual(task_type_a.call_count, n_jobs_a) n_jobs_b = 4 jobs_b, calls_b = TestWorker.new_jobs(n_jobs_b, prefix="b") @@ -127,7 +127,7 @@ def test_execute_job_subsequent_success(self): self.service.execute_job_group(job_group.export_to_dict())) cms.service.Worker.get_task_type.assert_has_calls(calls_b) - self.assertEquals(task_type_b.call_count, n_jobs_b) + self.assertEqual(task_type_b.call_count, n_jobs_b) def test_execute_job_subsequent_locked(self): """Executes a long job, then another one that should fail @@ -173,7 +173,7 @@ def test_execute_job_failure_releases_lock(self): JobGroup.import_from_dict( self.service.execute_job_group(job_group.export_to_dict())) cms.service.Worker.get_task_type.assert_has_calls(calls_a) - self.assertEquals(task_type_a.call_count, n_jobs_a) + self.assertEqual(task_type_a.call_count, n_jobs_a) n_jobs_b = 3 jobs_b, calls_b = TestWorker.new_jobs(n_jobs_b) @@ -186,7 +186,7 @@ def test_execute_job_failure_releases_lock(self): self.service.execute_job_group(job_group.export_to_dict())) cms.service.Worker.get_task_type.assert_has_calls(calls_b) - self.assertEquals(task_type_b.call_count, n_jobs_b) + self.assertEqual(task_type_b.call_count, n_jobs_b) def test_execute_job_group_success(self): """Executes two successful job groups. @@ -202,7 +202,7 @@ def test_execute_job_group_success(self): self.service.execute_job_group(job_group.export_to_dict())) cms.service.Worker.get_task_type.assert_has_calls(calls) - self.assertEquals(task_type.call_count, sum(n_jobs)) + self.assertEqual(task_type.call_count, sum(n_jobs)) def test_execute_job_group_mixed_success(self): """Executes three job groups with mixed grades of success. @@ -213,7 +213,7 @@ def test_execute_job_group_mixed_success(self): [True] * n_jobs[0] + [False] + [True] * (n_jobs[1] - 1) + [False] * n_jobs[2]) - self.assertEquals(sum(n_jobs), len(expected_success)) + self.assertEqual(sum(n_jobs), len(expected_success)) job_groups, calls = TestWorker.new_job_groups(n_jobs) task_type = FakeTaskType(expected_success) @@ -231,7 +231,7 @@ def test_execute_job_group_mixed_success(self): expected_idx += 1 cms.service.Worker.get_task_type.assert_has_calls(calls) - self.assertEquals(task_type.call_count, sum(n_jobs)) + self.assertEqual(task_type.call_count, sum(n_jobs)) def test_execute_job_group_mixed_exceptions(self): """Executes a job group with some exceptions. @@ -239,7 +239,7 @@ def test_execute_job_group_mixed_exceptions(self): """ n_jobs = 4 expected_success = [True, Exception(), False, True] - self.assertEquals(n_jobs, len(expected_success)) + self.assertEqual(n_jobs, len(expected_success)) job_groups, unused_calls = TestWorker.new_job_groups([n_jobs]) task_type = FakeTaskType(expected_success) From a775792a804e3d82307c73f4334c972206820d9f Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Sat, 13 May 2023 12:36:19 +0200 Subject: [PATCH 12/43] Optimize docker build time Split `docker build` and `docker run` in two separate workflow steps to reduce the output size in the github action and make it more readable Also improve the docker (re)build speed by better using the cache: we first copy the requirements.txt, then install, then copy the rest of the folder, which means we won't invalidate the cached `pip install` step unless we really need to --- .github/workflows/main.yml | 6 +++++- Dockerfile | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e454f39c5..3b65f14ae4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,9 +17,13 @@ jobs: run: | mount | grep cgroup + - name: Build docker image + run: | + docker-compose -f docker-compose.test.yml build cms_test + - name: Run tests run: | - docker-compose -f docker-compose.test.yml run cms_test + docker-compose -f docker-compose.test.yml run --rm cms_test - uses: codecov/codecov-action@v3 with: diff --git a/Dockerfile b/Dockerfile index a6eb9d5f72..25710df4fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,12 +35,16 @@ RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers # Set cmsuser as default user USER cmsuser -COPY --chown=cmsuser:cmsuser . /home/cmsuser/cms +RUN mkdir /home/cmsuser/cms +COPY --chown=cmsuser:cmsuser requirements.txt dev-requirements.txt /home/cmsuser/cms/ WORKDIR /home/cmsuser/cms RUN sudo pip3 install -r requirements.txt RUN sudo pip3 install -r dev-requirements.txt + +COPY --chown=cmsuser:cmsuser . /home/cmsuser/cms + RUN sudo python3 setup.py install RUN sudo python3 prerequisites.py --yes --cmsuser=cmsuser install From 3896952508a17aeb0e157dec682b25da73d33736 Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Sat, 13 May 2023 12:36:44 +0200 Subject: [PATCH 13/43] Improve documentation on how to run tests Also update gitignore / dockerignore --- .dockerignore | 1 + .gitignore | 1 + codecov/.gitkeep | 0 docs/Docker image.rst | 34 ++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 5 files changed, 37 insertions(+) create mode 100644 codecov/.gitkeep create mode 100644 docs/Docker image.rst diff --git a/.dockerignore b/.dockerignore index ecbeca19d9..50d39eda50 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,4 @@ Dockerfile docker-compose*.yml .vagrant/ +codecov/ diff --git a/.gitignore b/.gitignore index 5b1d9142ca..81939ad6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ cache/ lib/ log/ .vagrant/ +codecov/ diff --git a/codecov/.gitkeep b/codecov/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/Docker image.rst b/docs/Docker image.rst new file mode 100644 index 0000000000..74e54a1ae6 --- /dev/null +++ b/docs/Docker image.rst @@ -0,0 +1,34 @@ +Docker image +************ + +We provide a docker image (defined in :gh_blob:`Dockerfile`) that can be used to +easily get an instance of CMS running locally with all the necessary +dependencies. We also provide a :gh_blob:`docker-compose.test.yml` files that +uses said docker image to run the tests. + +.. _docker-image_running-tests: + +Running tests +============= + +First you need to build the image: + +.. sourcecode:: bash + + sudo docker-compose -f docker-compose.test.yml build cms_test + +Then you can run the tests: + +.. sourcecode:: bash + + sudo docker-compose -f docker-compose.test.yml run --rm cms_test + +This command will create a ``cms_test_db`` container for the database which +**will not** be automatically deleted, and a ``cms_test`` container for CMS +which will be automatically deleted (because of the ``--rm`` flag) upon exiting. + +To delete the ``cms_test_db`` container after testing you can run: + +.. sourcecode:: bash + + sudo docker rm -f cms_test_db diff --git a/docs/index.rst b/docs/index.rst index fdfa658860..f12ed175f4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,7 @@ Welcome to CMS's documentation! :maxdepth: 2 Introduction + Docker image Installation Running CMS Data model From 8fad02de0b39641a67ef8b22f74b3066599b9803 Mon Sep 17 00:00:00 2001 From: Jongseo Lee Date: Sat, 29 Jul 2023 19:11:02 +0900 Subject: [PATCH 14/43] Enhance Korean translation (#1237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ":" after 총점 * 제출 시간 -> 제출 시각 * info * password sentence fix --- cms/locale/ko/LC_MESSAGES/cms.po | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cms/locale/ko/LC_MESSAGES/cms.po b/cms/locale/ko/LC_MESSAGES/cms.po index 373a7a19be..a27b326666 100644 --- a/cms/locale/ko/LC_MESSAGES/cms.po +++ b/cms/locale/ko/LC_MESSAGES/cms.po @@ -2,6 +2,7 @@ # Copyright (C) 2020 CMS development group # This file is distributed under the same license as the Contest Management # System project. +# Jongseo Lee , 2023.07.10. # Junu Kwon , 2021.01.12. # Hyun-seok Jeon , 2020.11.29. # Myungwoo Chun , 2016-2017. @@ -12,9 +13,9 @@ msgid "" msgstr "" "Project-Id-Version: Contest Management System 1.5.dev0\n" "Report-Msgid-Bugs-To: contestms@googlegroups.com\n" -"POT-Creation-Date: 2021-01-12 11:45+0900\n" -"PO-Revision-Date: 2020-01-12 11:45+0900\n" -"Last-Translator: Junu Kwon \n" +"POT-Creation-Date: 2023-07-10 23:49+0900\n" +"PO-Revision-Date: 2023-07-10 23:49+0900\n" +"Last-Translator: Jongseo Lee \n" "Language: ko\n" "Language-Team: \n" "Plural-Forms: nplurals=1; plural=0\n" @@ -371,11 +372,11 @@ msgstr "너무 자주 제출했습니다." #, python-format msgid "Among all tasks, you can submit again after %d seconds from last submission." -msgstr "누적 마지막 제출 시간을 기준으로 %d 초 이후에 다시 제출할 수 있습니다." +msgstr "누적 마지막 제출 시각을 기준으로 %d 초 이후에 다시 제출할 수 있습니다." #, python-format msgid "For this task, you can submit again after %d seconds from last submission." -msgstr "이 문제에 대해서, 마지막 제출 시간을 기준으로 %d 초 이후에 다시 제출할 수 있습니다." +msgstr "이 문제에 대해서, 마지막 제출 시각을 기준으로 %d 초 이후에 다시 제출할 수 있습니다." msgid "Invalid archive format!" msgstr "압축 파일 오류!" @@ -412,11 +413,11 @@ msgstr "너무 자주 테스트했습니다." #, python-format msgid "Among all tasks, you can test again after %d seconds from last test." -msgstr "마지막 채점 테스트 제출 시간을 기준으로 %d 초 이후에 다시 테스트를 제출할 수 있습니다." +msgstr "마지막 채점 테스트 제출 시각을 기준으로 %d 초 이후에 다시 테스트를 제출할 수 있습니다." #, python-format msgid "For this task, you can test again after %d seconds from last test." -msgstr "이 문제에 대한, 마지막 채점 테스트 제출 시간을 기준으로 %d 초 이후에 다시 테스트를 제출할 수 있습니다." +msgstr "이 문제에 대한, 마지막 채점 테스트 제출 시각을 기준으로 %d 초 이후에 다시 테스트를 제출할 수 있습니다." msgid "Invalid test format!" msgstr "채점 테스트 파일 오류!" @@ -863,7 +864,7 @@ msgid "Your username is:" msgstr "사용자ID:" msgid "The password you chose was stored securely." -msgstr "비밀번호:" +msgstr "선택한 비밀번호가 안전하게 저장되었습니다." msgid "Back to login" msgstr "로그인하기" @@ -908,7 +909,7 @@ msgid "Public score:" msgstr "공식 점수:" msgid "Total score:" -msgstr "총점" +msgstr "총점:" msgid "Score" msgstr "점수" @@ -1000,7 +1001,7 @@ msgid "Submit a solution" msgstr "채점 제출" msgid "You may submit any subset of outputs in a single submission." -msgstr "한 번 채점 제출로 모든 부분 채점을 수행할 수 있습니다." +msgstr "한 번의 채점 제출로 모든 부분 채점을 수행할 수 있습니다." #, python-format msgid "You can submit %(submissions_left)s more solution(s)." From 2af71ca8e052a574273b4a186a801706be373860 Mon Sep 17 00:00:00 2001 From: Ranald Lam Date: Fri, 4 Sep 2020 23:58:03 +0800 Subject: [PATCH 15/43] Add Notice task type --- cms/grading/tasktypes/Notice.py | 65 +++++++++++++++++++ cms/grading/tasktypes/abc.py | 1 + cms/server/contest/handlers/tasksubmission.py | 4 ++ cms/server/contest/handlers/taskusertest.py | 2 + cms/server/contest/templates/contest.html | 9 ++- cms/server/contest/templates/overview.html | 11 +++- cms/service/ProxyService.py | 3 + .../unit_tests/cmscontrib/ImportTaskTest.py | 4 +- cmstestsuite/unit_tests/databasemixin.py | 12 ++-- setup.py | 1 + 10 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 cms/grading/tasktypes/Notice.py diff --git a/cms/grading/tasktypes/Notice.py b/cms/grading/tasktypes/Notice.py new file mode 100644 index 0000000000..356f9acb65 --- /dev/null +++ b/cms/grading/tasktypes/Notice.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2017 Amir Keivan Mohtashami +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Task type for notice tasks. +""" +import logging + +from . import TaskType + +logger = logging.getLogger(__name__) + + +# Dummy function to mark translatable string. +def N_(message): + return message + + +class Notice(TaskType): + + ACCEPTED_PARAMETERS = [] + ALLOW_SUBMISSION = False + + testable = False + + def get_compilation_commands(self, unused_submission_format): + """See TaskType.get_compilation_commands.""" + return None + + def get_user_managers(self, unused_submission_format): + """See TaskType.get_user_managers.""" + return [] + + def get_auto_managers(self): + """See TaskType.get_auto_managers.""" + return [] + + def compile(self, job, file_cacher): + """See TaskType.compile.""" + # No compilation needed. + job.success = True + job.compilation_success = True + job.text = [N_("No compilation needed")] + job.plus = {} + + def evaluate(self, job, file_cacher): + job.success = True + job.outcome = "0.0" + job.text = "" + job.plus = {} diff --git a/cms/grading/tasktypes/abc.py b/cms/grading/tasktypes/abc.py index 9130a9e0f6..a17f88f9b3 100644 --- a/cms/grading/tasktypes/abc.py +++ b/cms/grading/tasktypes/abc.py @@ -56,6 +56,7 @@ class TaskType(metaclass=ABCMeta): # submit only some of the required files; moreover, we try to fill # the non-provided files with the one in the previous submission. ALLOW_PARTIAL_SUBMISSION = False + ALLOW_SUBMISSION = True # A list of all the accepted parameters for this task type. # Each item is an instance of TaskTypeParameter. diff --git a/cms/server/contest/handlers/tasksubmission.py b/cms/server/contest/handlers/tasksubmission.py index 9199c0bcd7..eb993f3501 100644 --- a/cms/server/contest/handlers/tasksubmission.py +++ b/cms/server/contest/handlers/tasksubmission.py @@ -119,6 +119,10 @@ def get(self, task_name): if task is None: raise tornado_web.HTTPError(404) + task_type = task.active_dataset.task_type_object + if not task_type.ALLOW_SUBMISSION: + raise tornado.web.HTTPError(404) + submissions = self.sql_session.query(Submission)\ .filter(Submission.participation == participation)\ .filter(Submission.task == task)\ diff --git a/cms/server/contest/handlers/taskusertest.py b/cms/server/contest/handlers/taskusertest.py index c78c87c1c8..7224d11447 100644 --- a/cms/server/contest/handlers/taskusertest.py +++ b/cms/server/contest/handlers/taskusertest.py @@ -84,6 +84,8 @@ def get(self): for task in self.contest.tasks: if self.get_argument("task_name", None) == task.name: default_task = task + if default_task is None and task.active_dataset.task_type_object.testable: + default_task = task user_tests[task.id] = self.sql_session.query(UserTest)\ .filter(UserTest.participation == participation)\ .filter(UserTest.task == task)\ diff --git a/cms/server/contest/templates/contest.html b/cms/server/contest/templates/contest.html index 99f3dafd64..5ffb620816 100644 --- a/cms/server/contest/templates/contest.html +++ b/cms/server/contest/templates/contest.html @@ -185,9 +185,12 @@

{% trans %}Statement{% endtrans %} - - {% trans %}Submissions{% endtrans %} - + {% set task_type = get_task_type(dataset=t_iter.active_dataset) %} + {% if task_type.ALLOW_SUBMISSION %} + + {% trans %}Submissions{% endtrans %} + + {% endif %} {% endfor %} {% endif %}
  • diff --git a/cms/server/contest/templates/overview.html b/cms/server/contest/templates/overview.html index dba269e208..5c61ba4ba6 100644 --- a/cms/server/contest/templates/overview.html +++ b/cms/server/contest/templates/overview.html @@ -224,8 +224,15 @@

    {% trans %}Task overview{% endtrans %}

    {% trans %}N/A{% endtrans %} {% endif %} - {{ get_task_type(dataset=t_iter.active_dataset).name }} - {{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }} + {% set task_type = get_task_type(dataset=t_iter.active_dataset) %} + {{ task_type.name }} + + {% if task_type.ALLOW_SUBMISSION %} + {{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }} + {% else %} + {% trans %}N/A{% endtrans %} + {% endif %} + {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} {% if t_iter.token_mode == TOKEN_MODE_FINITE or t_iter.token_mode == TOKEN_MODE_INFINITE %} diff --git a/cms/service/ProxyService.py b/cms/service/ProxyService.py index 68b30271ab..320bbb248b 100644 --- a/cms/service/ProxyService.py +++ b/cms/service/ProxyService.py @@ -362,6 +362,9 @@ def initialize(self): tasks = dict() for task in contest.tasks: + task_type = task.active_dataset.task_type_object + if not task_type.ALLOW_SUBMISSION: + continue score_type = task.active_dataset.score_type_object tasks[encode_id(task.name)] = { "short_name": task.name, diff --git a/cmstestsuite/unit_tests/cmscontrib/ImportTaskTest.py b/cmstestsuite/unit_tests/cmscontrib/ImportTaskTest.py index 867666fedf..9a82645f53 100755 --- a/cmstestsuite/unit_tests/cmscontrib/ImportTaskTest.py +++ b/cmstestsuite/unit_tests/cmscontrib/ImportTaskTest.py @@ -192,7 +192,7 @@ def test_task_exists_update_overwrite_dataset(self): # check by looking at the task type. new_title = "new_title" new_task = self.get_task(name=self.task_name, title=new_title) - new_task_type = "Batch" + new_task_type = "OutputOnly" self.get_dataset(task=new_task, description=self.dataset_description, task_type=new_task_type) self.assertNotEqual(new_task_type, self.dataset.task_type) @@ -203,7 +203,7 @@ def test_task_exists_update_overwrite_dataset(self): self.assertTaskInDb(self.task_name, new_title, self.contest_id, task_id=self.task_id, dataset_descriptions=[self.dataset_description], - dataset_task_types=["Batch"]) + dataset_task_types=["OutputOnly"]) def test_task_exists_update_new_manager(self): # Task exists, and we update it, attaching it to the same contest. diff --git a/cmstestsuite/unit_tests/databasemixin.py b/cmstestsuite/unit_tests/databasemixin.py index 94ef43daa5..9ecf1c7f20 100644 --- a/cmstestsuite/unit_tests/databasemixin.py +++ b/cmstestsuite/unit_tests/databasemixin.py @@ -161,14 +161,10 @@ def get_dataset(cls, task=None, **kwargs): args = { "task": task, "description": unique_unicode_id(), - "task_type": "", - # "None" won't work here as the column is defined as non - # nullable. As soon as we'll depend on SQLAlchemy 1.1 we - # will be able to put JSON.NULL here instead. - "task_type_parameters": {}, - "score_type": "", - # Same here. - "score_type_parameters": {}, + "task_type": "Batch", + "task_type_parameters": ["alone", ["", ""], "diff"], + "score_type": "Sum", + "score_type_parameters": 100, } args.update(kwargs) dataset = Dataset(**args) diff --git a/setup.py b/setup.py index 15c0a7d5a0..c3af49f275 100755 --- a/setup.py +++ b/setup.py @@ -174,6 +174,7 @@ def run(self): "Communication=cms.grading.tasktypes.Communication:Communication", "OutputOnly=cms.grading.tasktypes.OutputOnly:OutputOnly", "TwoSteps=cms.grading.tasktypes.TwoSteps:TwoSteps", + "Notice=cms.grading.tasktypes.Notice:Notice", ], "cms.grading.scoretypes": [ "Sum=cms.grading.scoretypes.Sum:Sum", From 5ceac7bf865dd9864d98daaeb3ab232bcdaf9891 Mon Sep 17 00:00:00 2001 From: Agus Sentosa Hermawan Date: Sat, 13 Feb 2021 15:28:09 +0700 Subject: [PATCH 16/43] Save the first time each submission is scored --- cms/db/submission.py | 5 +++++ cms/server/admin/static/aws_style.css | 4 ++++ .../admin/templates/macro/submission.html | 6 ++++++ cms/server/admin/templates/submission.html | 20 ++++++++++++------- cms/service/ScoringService.py | 3 +++ .../unit_tests/service/ScoringServiceTest.py | 7 +++++++ 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/cms/db/submission.py b/cms/db/submission.py index ab2f392d20..7ee045ae84 100644 --- a/cms/db/submission.py +++ b/cms/db/submission.py @@ -363,6 +363,11 @@ class SubmissionResult(Base): JSONB, nullable=True) + # Time when the submission is scored for the first time + scored_at = Column( + DateTime, + nullable=True) + # The same as the last two fields, but only showing information # visible to the user (assuming they did not use a token on this # submission). diff --git a/cms/server/admin/static/aws_style.css b/cms/server/admin/static/aws_style.css index 808d4c8d70..d983773ed9 100644 --- a/cms/server/admin/static/aws_style.css +++ b/cms/server/admin/static/aws_style.css @@ -394,6 +394,10 @@ table td.partial::after { content: "*"; } +table td#latency { + text-align: right; +} + table.sortable th { white-space: nowrap; } diff --git a/cms/server/admin/templates/macro/submission.html b/cms/server/admin/templates/macro/submission.html index 75a81f4620..ea884ddd63 100644 --- a/cms/server/admin/templates/macro/submission.html +++ b/cms/server/admin/templates/macro/submission.html @@ -30,6 +30,7 @@ User Task Status + Latency Files Token Official @@ -145,6 +146,11 @@

    Compilation output

    {# TODO: trim long outputs and add facility to see ra {% endif %} + + {% if sr is not none and sr.scored_at is not none %} + {{ (sr.scored_at - s.timestamp).total_seconds()|round|int }} s + {% endif %} + {% for filename, sub_file in s.files|dictsort(by="key") %} {% set real_filename = filename if s.language is none else filename|replace(".%l", (s.language|to_language).source_extension) %} diff --git a/cms/server/admin/templates/submission.html b/cms/server/admin/templates/submission.html index c052b91ff5..3751f97ca2 100644 --- a/cms/server/admin/templates/submission.html +++ b/cms/server/admin/templates/submission.html @@ -105,22 +105,22 @@

    Submission details

    Status {% if status == SubmissionResult.COMPILING %} - Compiling... + Compiling... {% elif status == SubmissionResult.COMPILATION_FAILED %} - Compilation failed + Compilation failed {% elif status == SubmissionResult.EVALUATING %} - Evaluating... + Evaluating... {% elif status == SubmissionResult.SCORING %} - Scoring... + Scoring... {% elif status == SubmissionResult.SCORED %} {% if st is defined %} {% set max_score = st.max_score %} {% else %} {% set max_score = "[Cannot get score type - see logs]" %} {% endif %} - Scored ({{ sr.score }} / {{ max_score }}) + Scored ({{ sr.score }} / {{ max_score }}) {% else %} - N/A + N/A {% endif %} @@ -136,7 +136,13 @@

    Submission details

    {% endif %} - {% if sr is not none %} + {% if sr is not none and sr.scored_at is not none %} + + Latency as seen by user + + {{ (sr.scored_at - s.timestamp).total_seconds()|round|int }} s + + Failures during compilation {{ sr.compilation_tries }} diff --git a/cms/service/ScoringService.py b/cms/service/ScoringService.py index b7657305b1..633ab2a48a 100644 --- a/cms/service/ScoringService.py +++ b/cms/service/ScoringService.py @@ -100,6 +100,9 @@ def execute(self, entry): submission_result.ranking_score_details = \ score_type.compute_score(submission_result) + if submission_result.scored_at is None: + submission_result.scored_at = make_datetime() + # Store it. session.commit() diff --git a/cmstestsuite/unit_tests/service/ScoringServiceTest.py b/cmstestsuite/unit_tests/service/ScoringServiceTest.py index acf28599a2..872a6fd33b 100755 --- a/cmstestsuite/unit_tests/service/ScoringServiceTest.py +++ b/cmstestsuite/unit_tests/service/ScoringServiceTest.py @@ -24,6 +24,9 @@ # We enable monkey patching to make many libraries gevent-friendly # (for instance, urllib3, used by requests) import gevent.monkey + +from cmscommon.datetime import make_datetime + gevent.monkey.patch_all() # noqa import unittest @@ -101,6 +104,7 @@ def test_new_evaluation(self): sr.public_score, sr.public_score_details, sr.ranking_score_details), self.score_info) + self.assertIsNotNone(sr.scored_at) def test_new_evaluation_two(self): """More than one submissions in the queue. @@ -126,6 +130,8 @@ def test_new_evaluation_already_scored(self): """ sr = self.new_sr_scored() + current_time = make_datetime() + sr.scored_at = current_time self.session.commit() service = ScoringService(0) @@ -135,6 +141,7 @@ def test_new_evaluation_already_scored(self): # Asserts that compute_score was called. self.score_type.compute_score.assert_not_called() + self.assertEqual(current_time, sr.scored_at) if __name__ == "__main__": From 3325f394680f69f3de69672389af56dabc13cff9 Mon Sep 17 00:00:00 2001 From: Agus Sentosa Hermawan Date: Sun, 21 Feb 2021 14:24:40 +0700 Subject: [PATCH 17/43] Change scoreboard gradient to white to green --- cmsranking/static/Ranking.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cmsranking/static/Ranking.css b/cmsranking/static/Ranking.css index 7ddfafa996..9d857e2d30 100644 --- a/cmsranking/static/Ranking.css +++ b/cmsranking/static/Ranking.css @@ -802,62 +802,62 @@ abbr { } .score_0 { - background-color: #F40000; + background-color: #ffffff; /*background-image: linear-gradient(rgb(204, 0, 0), rgb(244, 0, 0));*/ } .score_0_10 { - background-color: #EB1D00; + background-color: #ecf4e4; /*background-image: linear-gradient(rgb(210, 38, 0), rgb(235, 29, 0));*/ } .score_10_20 { - background-color: #E23A00; + background-color: #e5f0da; /*background-image: linear-gradient(rgb(216, 77, 0), rgb(226, 58, 0));*/ } .score_20_30 { - background-color: #D95700; + background-color: #cfe4bc; /*background-image: linear-gradient(rgb(222, 115, 0), rgb(217, 87, 0));*/ } .score_30_40 { - background-color: #D17400; + background-color: #c3ddaa; /*background-image: linear-gradient(rgb(228, 154, 0), rgb(209, 116, 0));*/ } .score_40_50 { - background-color: #C89100; + background-color: #daeacb; /*background-image: linear-gradient(rgb(234, 192, 0), rgb(200, 145, 0));*/ } .score_50_60 { - background-color: #B99F00; + background-color: #b5d596; /*background-image: linear-gradient(rgb(225, 211, 2), rgb(185, 159, 0));*/ } .score_60_70 { - background-color: #A39E01; + background-color: #a7cd82; /*background-image: linear-gradient(rgb(203, 211, 6), rgb(163, 158, 1));*/ } .score_70_80 { - background-color: #8E9D02; + background-color: #7ab444; /*background-image: linear-gradient(rgb(181, 211, 10), rgb(142, 157, 2));*/ } .score_80_90 { - background-color: #789C03; + background-color: #8cbe5d; /*background-image: linear-gradient(rgb(159, 210, 14), rgb(120, 156, 3));*/ } .score_90_100 { - background-color: #639B04; + background-color: #68aa2b; /*background-image: linear-gradient(rgb(137, 210, 18), rgb(99, 155, 4));*/ } .score_100 { - background-color: #4E9A05; + background-color: #4e9a05; /*background-image: linear-gradient(rgb(115, 210, 22), rgb(78, 154, 5));*/ } From 2555b4c8d4fd1bf9e737ee9f38229f96b89ecf5c Mon Sep 17 00:00:00 2001 From: Jonathan Irvin Gunawan Date: Tue, 16 Mar 2021 23:01:59 +0800 Subject: [PATCH 18/43] Use SCORE_MODE_MAX_SUBTASK as default score_mode for tps loader --- cmscontrib/loaders/tps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmscontrib/loaders/tps.py b/cmscontrib/loaders/tps.py index aacbea9ee4..cf7d2cbf38 100644 --- a/cmscontrib/loaders/tps.py +++ b/cmscontrib/loaders/tps.py @@ -26,6 +26,7 @@ from datetime import timedelta from cms.db import Task, Dataset, Manager, Testcase, Attachment, Statement +from cmscommon.constants import SCORE_MODE_MAX_SUBTASK from .base_loader import TaskLoader @@ -123,6 +124,7 @@ def get_task(self, get_statement=True): args["name"] = name args["title"] = data['name'] + args["score_mode"] = SCORE_MODE_MAX_SUBTASK # Statements if get_statement: From e884a30a29c04ea24862525fbcafec3ebca2bb7c Mon Sep 17 00:00:00 2001 From: Agus Sentosa Hermawan Date: Fri, 24 Sep 2021 22:06:06 +0700 Subject: [PATCH 19/43] Allow contestant to see submission after the contest ends --- cms/server/contest/handlers/task.py | 6 ++-- cms/server/contest/handlers/tasksubmission.py | 8 ++--- cms/server/contest/templates/contest.html | 2 +- .../contest/templates/task_submissions.html | 29 ++++++++++--------- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/cms/server/contest/handlers/task.py b/cms/server/contest/handlers/task.py index 3f6f163a28..5ce121068c 100644 --- a/cms/server/contest/handlers/task.py +++ b/cms/server/contest/handlers/task.py @@ -49,7 +49,7 @@ class TaskDescriptionHandler(ContestHandler): """ @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name): task = self.get_task(task_name) @@ -64,7 +64,7 @@ class TaskStatementViewHandler(FileHandler): """ @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name, lang_code): task = self.get_task(task_name) @@ -90,7 +90,7 @@ class TaskAttachmentViewHandler(FileHandler): """ @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name, filename): task = self.get_task(task_name) diff --git a/cms/server/contest/handlers/tasksubmission.py b/cms/server/contest/handlers/tasksubmission.py index eb993f3501..017f9e84e9 100644 --- a/cms/server/contest/handlers/tasksubmission.py +++ b/cms/server/contest/handlers/tasksubmission.py @@ -110,7 +110,7 @@ class TaskSubmissionsHandler(ContestHandler): """ @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name): participation = self.current_user @@ -225,7 +225,7 @@ def add_task_score(self, participation, task, data): task.score_precision, translation=self.translation) @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name, submission_num): task = self.get_task(task_name) @@ -285,7 +285,7 @@ class SubmissionDetailsHandler(ContestHandler): refresh_cookie = False @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name, submission_num): task = self.get_task(task_name) @@ -326,7 +326,7 @@ class SubmissionFileHandler(FileHandler): """ @tornado_web.authenticated - @actual_phase_required(0, 3) + @actual_phase_required(0, 1, 2, 3, 4) @multi_contest def get(self, task_name, submission_num, filename): if not self.contest.submissions_download_allowed: diff --git a/cms/server/contest/templates/contest.html b/cms/server/contest/templates/contest.html index 5ffb620816..9f21acc713 100644 --- a/cms/server/contest/templates/contest.html +++ b/cms/server/contest/templates/contest.html @@ -177,7 +177,7 @@

    - {% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted %} + {% if actual_phase >= 0 or participation.unrestricted %} {% for t_iter in contest.tasks %}