diff --git a/ChangeLog b/ChangeLog index b33b02d59e..e01750de87 100644 --- a/ChangeLog +++ b/ChangeLog @@ -35,7 +35,8 @@ Version 8.0.0 - DEV - Improve API action to add submission to adhere to the CCS spec. - Add API action to create clarification following CCS spec. - Add support for showing (pending) awards on the scoreboard. Thanks @shuibinlong! - - Remove JudgehostRestriction feature due to new Judgehost API + - Remove JudgehostRestriction feature due to new Judgehost API. + - Replace C++ submit client with (standalone) Python version. Version 7.3.2 - 23 November 2020 diff --git a/Makefile b/Makefile index 4e88ffc89b..8c6f72c2b9 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,6 @@ default: @echo " - make build (all except 'docs')" @echo " - make domserver" @echo " - make judgehost" - @echo " - make submitclient" @echo " - make docs" @echo or @echo " - make install-domserver" @@ -39,19 +38,13 @@ install: all: build build: domserver judgehost -ifeq ($(SUBMITCLIENT_ENABLED),yes) -build: submitclient -endif - ifeq ($(BUILD_DOCS),yes) all: docs dist: distdocs endif # MAIN TARGETS -domserver judgehost docs submitclient: paths.mk config -submitclient: - $(MAKE) -C submit submitclient +domserver judgehost docs: paths.mk config install-domserver: domserver domserver-create-dirs install-judgehost: judgehost judgehost-create-dirs install-docs: docs-create-dirs @@ -327,6 +320,6 @@ clean-autoconf: .PHONY: $(addsuffix -create-dirs,domserver judgehost docs) check-root \ clean-autoconf $(addprefix maintainer-,conf install uninstall) \ - config submitclient distdocs composer-dependencies \ + config distdocs composer-dependencies \ composer-dependencies-dev \ coverity-conf coverity-build diff --git a/configure.ac b/configure.ac index 158d2955da..3cd79d079b 100644 --- a/configure.ac +++ b/configure.ac @@ -248,66 +248,6 @@ AC_PROG_LN_S AC_PROG_MAKE_SET AC_PROG_MKDIR_P -# Check for including optional libmagic. -AC_CHECK_LIB(magic,magic_open,AC_SUBST(LIBMAGIC,[-lmagic])) - -# {{{ submitclient - -AC_ARG_ENABLE(submitclient,AS_HELP_STRING([--disable-submitclient], -[enable submit client program (default: yes). - This requires JSONcpp and cURL libraries and optionally - libmagic for detecting submission of binary files.])) - -if test "x$enable_submitclient" != "xno"; then - AC_SUBST(SUBMITCLIENT_ENABLED,[yes]) - - # Check for libcURL and JSONcpp when submit is enabled. - AX_LIB_CURL([7.9.7],[],[ - AC_MSG_ERROR([libcURL not found (required for submit client)]) - ]) - save_CPPFLAGS="$CPPFLAGS" - CPPFLAGS="$CPPFLAGS $CURL_CFLAGS" - AC_CHECK_HEADERS([curl/curl.h],[],[ - AC_MSG_ERROR([libcURL headers not found (required for submit client)]) - ]) - CPPFLAGS="$save_CPPFLAGS" - # The 'curl-config --static-libs' option is accepted since 7.17.1 - AC_SUBST(CURL_STATIC,"") - if test "x$enable_static_linking" = "xyes"; then - AX_COMPARE_VERSION([$curl_version],[ge],[7.17.1],[ - AC_SUBST(CURL_STATIC,`curl-config --static-libs`)]) - fi - - # Check for JSONcpp library to decode language/extensions list from - # REST API (this also requires libcURL below). - - # Note that under Debian the header files are installed under - # PREFIX/jsoncpp/json/json.h and not PREFIX/json/json.h since that - # conflicts with the libjson C library, so we check that first. - AC_LANG_PUSH([C++]) - AC_CHECK_HEADERS([jsoncpp/json/json.h json/json.h],[found_jsoncpp_headers=yes; break;],[]) - AS_IF([test "x$found_jsoncpp_headers" != "xyes"], - AC_MSG_ERROR([JSONcpp headers not found (required for submit client)]), - AC_SUBST(LIBJSONCPP,[-ljsoncpp])) - AC_LANG_POP([C++]) - -fi -# }}} - -# Check option to link statically against "special" libraries for -# submit client. -AC_ARG_ENABLE(static-linking,AS_HELP_STRING([--enable-static-linking], -[enable static linking against libcurl, libmagic, libjsoncpp for 'submit' - (default: no).])) - -if test "x$enable_static_linking" = "xyes"; then - AC_SUBST(STATIC_LINK_START,[-Wl,-Bstatic]) - AC_SUBST(STATIC_LINK_END, [-Wl,-Bdynamic]) -else - AC_SUBST(STATIC_LINK_START,['']) - AC_SUBST(STATIC_LINK_END, ['']) -fi - # Check option to build documentation. This option is provided to # allow disabling it. AC_ARG_ENABLE(doc-build,AS_HELP_STRING([--enable-doc-build], @@ -373,12 +313,6 @@ echo " * webserver group.....: $WEBSERVER_GROUP" echo "" echo " * website base URL....: $BASEURL" echo "" -if test "x$SUBMITCLIENT_ENABLED" = xyes ; then - echo " * submitclient........: enabled, cURL version: $curl_version" -else - echo " * submitclient........: disabled" -fi -echo "" echo -n " * documentation.......: AX_VAR_EXPAND($domjudge_docdir)" if test "x$DOC_BUILD_ENABLED" != xyes ; then echo " (disabled)" diff --git a/doc/manual/install-workstation.rst b/doc/manual/install-workstation.rst index 4e62da51d4..1b862cb7d7 100644 --- a/doc/manual/install-workstation.rst +++ b/doc/manual/install-workstation.rst @@ -19,22 +19,39 @@ Command line submit client DOMjudge comes with a command line submit client which makes it really convenient for teams to submit their solutions to DOMjudge. -In order to build the submit client, you need libcURL, libJSONcpp and -optionally libmagic. To install this on Debian-like distributions:: +In order to use the submit client, you need Python, the python requests +library and optionally the python magic library installed on the team's +workstation. To install this on Debian-like distributions:: - sudo apt install libcurl4-gnutls-dev libjsoncpp-dev libmagic-dev + sudo apt install python3 python3-requests python3-magic Or on RedHat/CentOS/Fedora:: - sudo yum install libcurl-devel jsoncpp-devel file-devel + sudo yum install python3 python3-requests python3-magic -Then run (adapt the URL to your environment; look at the Config check -in the admin web interface if unsure):: +You can now copy this client from ``submit/submit`` to the workstations. - ./configure --enable-static-linking --with-baseurl="https://yourhost.example.edu/domjudge" - make submitclient +The submit client needs to know the base URL of the domserver where it should +submit to. You have three options to configure this: -You can now copy this client from ``submit/submit`` to the workstations. +* Set it as an environment variable called ``SUBMITBASEURL``, e.g. in + ``/etc/profile.d/``. +* Modify the ``submit/submit`` file and set the variable of ``baseurl`` + at the top. +* Let teams pass it using the ``--url`` argument. + +Note that the environment variable overrides the hardcoded variable at +the top of the file and the ``--url`` argument overrides both other options. + +The submit client will need to know to which contest to submit to. If there +is only one active contest, that will be used. If not, you have two options +to configure this: + +* Set it as an environment variable called ``SUBMITCONTEST``, e.g. in + ``/etc/profile.d/``. +* Let teams pass it using the ``--contest`` argument. + +Note that the ``--contest`` argument overrides the environment variable. In order for the client to authenticate to DOMjudge, credentials can be pre-provisioned in the file ``~/.netrc`` in the user's homedir, with example diff --git a/etc/Makefile b/etc/Makefile index ebb6c8b9db..3f5ab96ba5 100644 --- a/etc/Makefile +++ b/etc/Makefile @@ -16,7 +16,7 @@ include $(TOPDIR)/Makefile.global SECRETS = dbpasswords.secret restapi.secret symfony_app.secret initial_admin_password.secret SUBST_CONFIGS = apache.conf nginx-conf nginx-conf-inner domjudge-fpm.conf domserver-static.php \ - judgehost-static.php runguard-config.h submit-config.h \ + judgehost-static.php runguard-config.h \ sudoers-domjudge $(SUBST_CONFIGS): %: %.in $(TOPDIR)/paths.mk diff --git a/gitlab/base.sh b/gitlab/base.sh index 10fa50dbb4..272a1ed039 100755 --- a/gitlab/base.sh +++ b/gitlab/base.sh @@ -48,7 +48,6 @@ parameters: domjudge.rundir: /output/run domjudge.tmpdir: /output/tmp domjudge.baseurl: http://localhost/domjudge - domjudge.submitclient_enabled: yes EOF # install all php dependencies diff --git a/gitlab/integration.sh b/gitlab/integration.sh index 4a02f5455d..724361295a 100755 --- a/gitlab/integration.sh +++ b/gitlab/integration.sh @@ -132,6 +132,7 @@ section_end more_setup section_start submitting "Submitting test sources (including Kattis example)" cd ${DIR}/tests +export SUBMITBASEURL='http://localhost/domjudge/' make check test-stress # Prepare to load example problems from Kattis/problemtools diff --git a/m4/ax_lib_curl.m4 b/m4/ax_lib_curl.m4 deleted file mode 100644 index c71ec8d5b9..0000000000 --- a/m4/ax_lib_curl.m4 +++ /dev/null @@ -1,38 +0,0 @@ -# =========================================================================== -# https://www.gnu.org/software/autoconf-archive/ax_lib_curl.html -# =========================================================================== -# -# SYNOPSIS -# -# AX_LIB_CURL([VERSION],[ACTION-IF-SUCCESS],[ACTION-IF-FAILURE]) -# -# DESCRIPTION -# -# Checks for minimum curl library version VERSION. If successful executes -# ACTION-IF-SUCCESS otherwise ACTION-IF-FAILURE. -# -# Defines CURL_LIBS and CURL_CFLAGS. -# -# A simple example: -# -# AX_LIB_CURL([7.19.4],,[ -# AC_MSG_ERROR([Your system lacks libcurl >= 7.19.4]) -# ]) -# -# This macro is a rearranged version of AC_LIB_CURL from Akos Maroy. -# -# LICENSE -# -# Copyright (c) 2009 Francesco Salvestrini -# -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice -# and this notice are preserved. This file is offered as-is, without any -# warranty. - -#serial 9 - -AU_ALIAS([AC_CHECK_CURL], [AX_LIB_CURL]) -AC_DEFUN([AX_LIB_CURL], [ - AX_PATH_GENERIC([curl],[$1],'s/^libcurl\ \+//',[$2],[$3]) -]) diff --git a/paths.mk.in b/paths.mk.in index cdfba93bb3..da2b0d7e25 100644 --- a/paths.mk.in +++ b/paths.mk.in @@ -26,9 +26,6 @@ CXXFLAGS = @CXXFLAGS@ CPPFLAGS = @CPPFLAGS@ LDFLAGS = @LDFLAGS@ @LIBS@ -STATIC_LINK_START = @STATIC_LINK_START@ -STATIC_LINK_END = @STATIC_LINK_END@ - EXEEXT = @EXEEXT@ OBJEXT = .@OBJEXT@ @@ -38,26 +35,12 @@ MKDIR_P = @MKDIR_P@ INSTALL = @INSTALL@ @SET_MAKE@ -# Build submit client? -SUBMITCLIENT_ENABLED = @SUBMITCLIENT_ENABLED@ - # Build documentation? DOC_BUILD_ENABLED = @DOC_BUILD_ENABLED@ # libcgroup LIBCGROUP = @LIBCGROUP@ -# libmagic -LIBMAGIC = @LIBMAGIC@ - -# libJSONcpp -LIBJSONCPP = @LIBJSONCPP@ - -# libcURL -CURL_CFLAGS = @CURL_CFLAGS@ -CURL_LIBS = @CURL_LIBS@ -CURL_STATIC = @CURL_STATIC@ - # User:group file ownership of password files DOMJUDGE_USER = @DOMJUDGE_USER@ WEBSERVER_GROUP = @WEBSERVER_GROUP@ @@ -167,7 +150,6 @@ define substconfigvars -e 's,@RUNUSER[@],@RUNUSER@,g' \ -e 's,@RUNGROUP[@],@RUNGROUP@,g' \ -e 's,@BASEURL[@],@BASEURL@,g' \ - -e 's,@SUBMITCLIENT_ENABLED[@],@SUBMITCLIENT_ENABLED@,g' \ > $@ @chmod --reference=$< $@ endef diff --git a/submit/.gitignore b/submit/.gitignore deleted file mode 100644 index 3fbbddba6f..0000000000 --- a/submit/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/submit -/submit.exe diff --git a/submit/Makefile b/submit/Makefile index 3691007b62..a6736989e3 100644 --- a/submit/Makefile +++ b/submit/Makefile @@ -5,50 +5,10 @@ include $(TOPDIR)/Makefile.global TARGETS = -SUBMITCLIENT = submit$(EXEEXT) -# Statically link against libmagic and libJSONcpp to prevent -# dependency on team workstations (this might be GCC specific): -ifneq ($(LIBMAGIC),) -$(SUBMITCLIENT): LDFLAGS += $(STATIC_LINK_START) $(LIBMAGIC) $(STATIC_LINK_END) -endif -ifneq ($(LIBJSONCPP),) -$(SUBMITCLIENT): LDFLAGS += $(STATIC_LINK_START) $(LIBJSONCPP) $(STATIC_LINK_END) -endif -$(SUBMITCLIENT): CXXFLAGS += $(CURL_CFLAGS) -# Try to link statically against libcURL to prevent dependency -# on team workstations: -ifneq ($(CURL_STATIC),) -$(SUBMITCLIENT): LDFLAGS += $(CURL_STATIC) -lpthread -else -$(SUBMITCLIENT): LDFLAGS += $(CURL_LIBS) -endif -$(SUBMITCLIENT): LDFLAGS := $(filter-out -pie,$(LDFLAGS)) - -SUBMITHEADERS = $(TOPDIR)/etc/submit-config.h - -ifeq ($(SUBMITCLIENT_ENABLED),yes) -submitclient: $(SUBMITCLIENT) -TARGETS += $(SUBMITCLIENT) -else -submitclient: - @echo "Submit client is disabled." - @exit 1 -endif - -# Explicitly link with C++ compiler, as without any C++ extensions -# these are recognized as plain C objects. -$(SUBMITCLIENT): %$(EXEEXT): %$(OBJEXT) $(LIBOBJECTS) - $(CXX) $^ -o $@ $(LDFLAGS) - -$(SUBMITCLIENT:%=%$(OBJEXT)): %$(OBJEXT): %.cc $(SUBMITHEADERS) $(LIBHEADERS) - -clean-l: - -rm -f $(TARGETS) $(TARGETS:%=%$(OBJEXT)) - -check: submitclient +check: bats submit_standalone.bats -check-full: submitclient +check-full: bats submit_standalone.bats submit_online.bats -.PHONY: submitclient check check-full +.PHONY: check check-full diff --git a/submit/basename.h b/submit/basename.h deleted file mode 100644 index 1ab4920353..0000000000 --- a/submit/basename.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * basename - return the name-within-directory of a file name. - * Inspired by basename.c from the GNU C Library. - * - * Part of the DOMjudge Programming Contest Jury System and licensed - * under the GNU GPL. See README and COPYING for details. - */ - -#include - -#if defined(__CYGWIN__) || defined(__CYGWIN32__) -#define PATHSEP "\\/" -#else -#define PATHSEP "/" -#endif - -const char *gnu_basename(const char *filename) -{ - const char *p; - - for(p=filename+strlen(filename)-1; p>=filename; p--) { - if ( strchr(PATHSEP,*p)!=NULL ) break; - } - - return p+1; -} diff --git a/submit/submit b/submit/submit new file mode 100755 index 0000000000..05ceedd9ad --- /dev/null +++ b/submit/submit @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 + +# +# submit -- command-line submit program for submissions. +# +# Part of the DOMjudge Programming Contest Jury System and licensed +# under the GNU GPL. See README and COPYING for details. +# + +import argparse +import json +import logging +import os +import requests +import stat +import sys +import time +try: + import magic +except ModuleNotFoundError: + # Ignore, magic is optional + magic = None + +# Set the default base URL to submit to (optional). It can be overridden +# by the SUBMITBASEURL environment variable or the -u/--url argument. +baseurl = '' + +# Use a specific API version, set to empty string for default +# or set to the version followed by a slash to use that version +api_version = '' + +# Last modified time in minutes after which to warn for submitting +# an old file. +warn_mtime_minutes = 5 + +# End of configurable settings +num_warnings = 0 + +def confirm(message: str) -> bool: + answer = '' + while answer not in ['y', 'n']: + answer = input(F'{message} (y/n) ').lower() + return answer == 'y' + +def warn_user(msg: str): + global num_warnings + if args.quiet: + logging.debug(F'user warning #{num_warnings}: {msg}') + else: + print(F'WARNING: {msg}!') + num_warnings += 1 + +def usage(msg: str): + logging.error(F'error: {msg}') + + print(F"Type '{sys.argv[0]} --help' to get help.") + + exit(1) + +def error(msg: str): + logging.error(msg) + exit(-1) + +def read_contests() -> list: + '''Read all contests from the API + + Returns: + The contests or None if an error occurred + ''' + + try: + data = do_api_request('contests') + except RuntimeError as e: + logging.warning(e) + return None + + if not isinstance(data, list): + logging.warning('REST API returned unexpected JSON data for endpoint contests') + return None + + contests = [] + + for contest in data: + if ('id' not in contest + or 'shortname' not in contest + or not contest['id'] + or not contest['shortname']): + logging.warning('REST API returned unexpected JSON data for contests') + return None + contests.append(contest) + + logging.info(F'read {len(contests)} contests from the API') + + return contests + +def read_languages() -> list: + '''Read all languages for the current contest from the API + + Returns: + The languages or None if an error occurred + ''' + + try: + endpoint = 'contests/' + my_contest['id'] + '/languages' + data = do_api_request(endpoint) + except RuntimeError as e: + logging.warning(e) + return None + + if not isinstance(data, list): + logging.warning('REST API returned unexpected JSON data for endpoint languages') + return None + + languages = [] + + for item in data: + if ('id' not in item + or 'extensions' not in item + or not item['id'] + or not isinstance(item['extensions'], list) + or len(item['extensions']) == 0): + logging.warning('REST API returned unexpected JSON data for languages') + return None + language = { + 'id': item['id'], + 'name': item['name'], + 'entry_point_required': item['entry_point_required'] or False, + 'extensions': {item['id']} + } + for extension in item['extensions']: + language['extensions'].add(extension) + languages.append(language) + + logging.info(F'read {len(languages)} languages from the API') + + return languages + +def read_problems() -> list: + '''Read all problems for the current contest from the API + + Returns: + The problems or None if an error occurred + ''' + + try: + endpoint = 'contests/' + my_contest['id'] + '/problems' + data = do_api_request(endpoint) + except RuntimeError as e: + logging.warning(e) + return None + + if not isinstance(data, list): + logging.warning('REST API returned unexpected JSON data for endpoint problems') + return None + + problems = [] + + for problem in data: + if ('id' not in problem + or 'label' not in problem + or not problem['id'] + or not problem['label']): + logging.warning('REST API returned unexpected JSON data for problems') + return None + problems.append(problem) + + logging.info(F'read {len(problems)} problems from the API') + + return problems + +def do_api_request(name: str): + '''Perform an API call to the given endpoint and return its data + + Parameters: + name (str): the endpoint to call + + Returns: + The endpoint contents + + Raises: + RunTimeError when the response is not JSON or the HTTP status code is non 2xx + ''' + + if not baseurl: + raise RuntimeError('No baseurl set') + + url = F'{baseurl}api/{api_version}{name}' + + logging.info(F'Connecting to {url}') + + try: + response = requests.get(url) + except requests.exceptions.ConnectionError as e: + raise RuntimeError(e) + + if response.status_code >= 300: + print(response.text) + if response.status_code == 401: + raise RuntimeError('authentication failed, please check your DOMjudge credentials in ~/.netrc.') + else: + raise RuntimeError(F'API request {name} failed (code {response.status_code})') + + logging.debug(F"API call '{name}' returned:\n{response.text}") + + return json.loads(response.text) + +def kotlin_base_entry_point(filebase: str) -> str: + if filebase == "": + return "_" + chars = [c for c in filebase] + for idx, c in enumerate(chars): + if not c.isalnum(): + chars[idx] = "_" + + if chars[0].isalnum(): + chars[0] = chars[0].upper() + filebase = "".join(chars) + else: + filebase = "_" + "".join(chars) + + return filebase + +def get_epilog(): + '''Get the epilog for the help text''' + + contests_part_one = None + contests_part_two = None + + if not contests or len(contests) <= 1: + contests_part_one = '''For CONTEST use the ID or short name as shown in the top-right contest +selection box in the webinterface.''' + if contests and len(contests) == 1: + contests_part_two = F"Currently this defaults to the only active contest '{contests[0]['shortname']}'" + else: + contests_part_one = 'For CONTEST use one of the following:' + for contest in contests: + contests_part_one += F"\n {contest['shortname']:<10} - {contest['name']}" + + if not problems or len(problems) == 0: + problem_part = 'For PROBLEM use the label as on the scoreboard.' + else: + problem_part = 'For PROBLEM use one of the following:' + for problem in problems: + problem_part += F"\n {problem['label']:<10} - {problem['name']}" + + if not languages or len(languages) == 0: + language_part = 'For LANGUAGE use the ID or a common extension in lower- or uppercase.' + else: + language_part = 'For LANGUAGE use one of the following IDs/extensions in lower- or uppercase:' + for language in languages: + language_part += F"\n {language['name'] + ':':<20} {', '.join(sorted(language['extensions']))}" + + epilog_parts = [ + "Explanation of submission options:", + contests_part_one, + contests_part_two, + problem_part, + '''When not specified, PROBLEM defaults to the first FILENAME excluding the +extension. For example, 'B.java' will indicate problem 'B'.''', + language_part, + '''The default for LANGUAGE is the extension of FILENAME. For example, +'B.java' will indicate a Java solution.''', + "Set URL to the base address of the webinterface without the 'api/' suffix.\n" + + (F"The (pre)configured URL is '{baseurl}'\n" if baseurl else '') + + "Credentials are read from ~/.netrc (see netrc(4) for details).", + "Examples:", + F'''Submit problem 'b' in Java: + {sys.argv[0]} b.java''', + F'''Submit problem 'z' in C# for contest 'demo': + {sys.argv[0]} --contest=demo z.cs''', + F'''Submit problem 'e' in C++: + {sys.argv[0]} --problem e --language=cpp ProblemE.cc''', + F'''Submit problem 'hello' in C (options override the defaults from FILENAME): + {sys.argv[0]} -p hello -l C HelloWorld.cpp''', + F'''Submit multiple files (the problem and language are taken from the first): + {sys.argv[0]} hello.java message.java''', + ] + + return "\n\n".join(part for part in epilog_parts if part != None) + +def do_api_submit(): + '''Submit to the API with the given data''' + + url = F"{baseurl}api/{api_version}contests/{my_contest['id']}/submissions" + + data = { + 'problem': my_problem['id'], + 'language': my_language['id'], + } + if entry_point: + data['entry_point'] = entry_point + + files = [('code[]', open(filename, 'rb')) for filename in filenames] + + logging.info(F'connecting to {url}') + + response = requests.post(url, data=data, files=files) + + logging.debug(F"API call 'submissions' returned:\n{response.text}") + + # The connection worked, but we may have received an HTTP error + if response.status_code >= 300: + print(response.text) + if response.status_code == 401: + raise RuntimeError('authentication failed, please check your DOMjudge credentials in ~/.netrc.') + else: + raise RuntimeError(F'Submission failed (code {response.status_code})') + + # We got a successful HTTP response. It worked. + # But check that we indeed received a submission ID. + + try: + submission = json.loads(response.text) + except json.decoder.JSONDecodeError as e: + error(F'parsing REST API output: {e}') + + if (not isinstance(submission, dict) + or not 'id' in submission + or not isinstance(submission['id'], str)): + error('REST API returned unexpected JSON data') + + print(F"Submission received, id = s{submission['id']}") + +version_text = ''' +submit -- part of DOMjudge +Written by the DOMjudge developers + +DOMjudge comes with ABSOLUTELY NO WARRANTY. This is free software, and you +are welcome to redistribute it under certain conditions. See the GNU +General Public Licence for details. +''' + +loglevels = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL, +} + +# Note: we set add_help to false since we can only print the help text after parsing flags, since the help contains data needed from the API +parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description='Submit a solution for a problem.', add_help=False) +parser.add_argument('--version', action='version', version=version_text, help='output version information and exit') +parser.add_argument('-h', '--help', help='display this help and exit', action='store_true') +parser.add_argument('-c', '--contest', help='''submit for contest with ID or short name CONTEST. + Defaults to the value of the + environment variable 'SUBMITCONTEST'. + Mandatory when more than one contest is active.''') +parser.add_argument('-p', '--problem', help='submit for problem with ID or label PROBLEM') +parser.add_argument('-l', '--language', help='submit in language with ID LANGUAGE') +parser.add_argument('-e', '--entry_point', help='set an explicit entry_point, e.g. the java main class') +parser.add_argument('-v', '--verbose', help='increase verbosity', choices=loglevels.keys(), nargs='?', const='INFO', default='WARNING') +parser.add_argument('-q', '--quiet', help='suppress warning/info messages', action='store_true') +parser.add_argument('-y', '--assume-yes', help='suppress user input and assume yes', action='store_true') +parser.add_argument('-u', '--url', help='''submit to server with base address URL + (should not be necessary for normal use)''') +parser.add_argument('filename', nargs='*', help='filename(s) to submit') + +args = parser.parse_args() + +verbosity = args.verbose +if args.quiet: + verbosity = 'ERROR' + +logging.basicConfig(format=F'[%(asctime)s] {sys.argv[0]}[{os.getpid()}]: %(message)s', datefmt='%b %d %H:%M:%S', level=loglevels[verbosity]) + +contest_id = '' + +if 'SUBMITBASEURL' in os.environ: + baseurl = os.environ['SUBMITBASEURL'] +if 'SUBMITCONTEST' in os.environ: + contest_id = os.environ['SUBMITCONTEST'] + +if args.contest: + contest_id = args.contest +problem_id = args.problem or '' +language_id = args.language or '' +entry_point = args.entry_point +if args.url: + baseurl = args.url + +logging.info(F'set verbosity to {verbosity}') + +# Make sure that baseurl terminates with a '/' for later concatenation. +if baseurl and baseurl[-1:] != '/': + baseurl += '/' + +contests = read_contests() +if not contests: + logging.warning('could not obtain active contests') + +my_contest = None +my_language = None +my_problem = None + +if not contest_id: + if not contests or len(contests) == 0: + warn_user('no active contests found (and no contest specified)') + elif len(contests) == 1: + my_contest = contests[0] + else: + warn_user('multiple active contests found, please specify one') +elif contests: + contest_id = contest_id.lower() + for contest in contests: + if contest['id'].lower() == contest_id or contest['shortname'].lower() == contest_id: + my_contest = contest + break + +languages = None +problems = None +if my_contest: + languages = read_languages() + problems = read_problems() + +parser.epilog = get_epilog() + +if args.help: + parser.print_help() + exit(0) + +if not baseurl: + usage('no url specified, pass it as --url or set as SUBMITBASEURL environment variable') + +if not my_contest: + usage('no (valid) contest specified, pass it as --contest or set as SUBMITCONTEST environment variable') + +if not languages: + logging.warning('could not obtain language data') + +if not problems: + logging.warning('could not obtain problem data') + +if len(args.filename) == 0: + usage('no file(s) specified') + +# Process all source files +filenames = [] +for index, filename in enumerate(args.filename): + # Ignore doubly specified files + if filename in filenames: + logging.debug(F"ignoring doubly specified file `{filename}'") + continue + + # Stat file and do some validation checks + try: + st = os.stat(filename) + except FileNotFoundError: + usage(F"Can not find file `{filename}'") + + logging.debug(F"submission file {index + 1}: `{filename}'") + + # Do some checks on submission file and warn user + if not stat.S_ISREG(st.st_mode): + warn_user(F"`{filename}' is not a regular file") + if not st.st_mode & stat.S_IRUSR: + warn_user(F"`{filename}' is not readable") + if st.st_size == 0: + warn_user(F"`{filename}' is empty") + + file_age = (time.time() - st.st_mtime) / 60 + if file_age > warn_mtime_minutes: + warn_user(F"`{filename}' has not been modified for {int(file_age)} minutes") + + if magic: + m = magic.from_file(filename, mime=True) + if m[:5] != 'text/': + warn_user(F"`{filename}' is detected as binary/data") + + filenames.append(filename) + +# Try to parse problem and language from first filename + +filebase = os.path.basename(filenames[0]) + +if '.' in filebase: + dot = filebase.rfind('.') + ext = filebase[dot+1:] + filebase = filebase[:dot] + + if not problem_id: + problem_id = filebase + if not language_id: + language_id = ext + +# Check for languages matching file extension +language_id = language_id.lower() +for language in languages: + for extension in language['extensions']: + if extension.lower() == language_id: + my_language = language + break + if my_language: + break + +if not my_language: + usage('no known language specified or detected') + +# Check for problem matching ID or label +problem_id = problem_id.lower() +for problem in problems: + if problem['id'].lower() == problem_id or problem['label'].lower() == problem_id: + my_problem = problem + break + +if not my_problem: + usage('no known problem specified or detected') + +# Guess entry point if not already specified. +if not entry_point and my_language['entry_point_required']: + if my_language['name'] == 'Java': + entry_point = filebase + elif my_language['name'] == 'Kotlin': + entry_point = kotlin_base_entry_point(filebase) + "Kt" + elif my_language['name'] == 'Python 3': + entry_point = filebase + "." + ext + +if not entry_point and my_language['entry_point_required']: + error('Entry point required but not specified nor detected.') + +logging.debug(F"contest is `{my_contest['shortname']}'") +logging.debug(F"problem is `{my_problem['label']}'") +logging.debug(F"language is `{my_language['name']}'") +logging.debug(F"entry_point is `{entry_point or ''}'") +logging.debug(F"url is `{baseurl}'") + +if not args.assume_yes: + print('Submission information:') + if len(filenames) == 1: + print(F' filename: {filenames[0]}') + else: + print(F' filenames: {" ".join(filenames)}') + print(F" contest: {my_contest['shortname']}") + print(F" problem: {my_problem['label']}") + print(F" language: {my_language['name']}") + if entry_point: + print(F' entry point: {entry_point}') + print(F' url: {baseurl}') + + if num_warnings > 0: + print('There are warnings for this submission!\a') + + if not confirm('Do you want to continue?'): + error('submission aborted by user') + +do_api_submit() diff --git a/submit/submit.cc b/submit/submit.cc deleted file mode 100644 index 80283dda0c..0000000000 --- a/submit/submit.cc +++ /dev/null @@ -1,975 +0,0 @@ -/* - * submit -- command-line submit program for solutions. - * - * Part of the DOMjudge Programming Contest Jury System and licensed - * under the GNU GPL. See README and COPYING for details. - * - */ - -#include "config.h" - -#include "submit-config.h" - -/* Check whether submit dependencies are available */ -#if ( ! ( HAVE_CURL_CURL_H && ( HAVE_JSONCPP_JSON_JSON_H || HAVE_JSON_JSON_H ) ) ) -#error "libcURL or libJSONcpp not available." -#endif - -/* Standard include headers */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef HAVE_JSONCPP_JSON_JSON_H -#include -#endif -#ifdef HAVE_JSON_JSON_H -#include -#endif -#ifdef HAVE_MAGIC_H -#include -#endif - - -/* C++ includes for easy string handling */ -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -/* These defines are needed in 'version' and 'logmsg' */ -#define DOMJUDGE_PROGRAM "DOMjudge/" DOMJUDGE_VERSION -#define PROGRAM "submit" -#define VERSION DOMJUDGE_VERSION "/" REVISION - -/* Use a specific API version, set to empty string for default */ -#define API_VERSION "v4/" - -/* Logging and error functions */ -#include "lib.error.h" - -/* Misc. other functions */ -#include "lib.misc.h" - -/* Include GNU version of basename() */ -#include "basename.h" - -const int timeout_secs = 60; /* seconds before send/receive timeouts with an error */ - -/* Variables defining logmessages verbosity to stderr/logfile */ -extern int verbose; -extern int loglevel; - -char *logfile; - -const char *progname; - -int quiet; -int assume_yes; -int show_help; -int show_version; - -struct option const long_opts[] = { - {"problem", required_argument, NULL, 'p'}, - {"language", required_argument, NULL, 'l'}, - {"url", required_argument, NULL, 'u'}, - {"verbose", optional_argument, NULL, 'v'}, - {"contest", required_argument, NULL, 'c'}, - {"entry_point", required_argument, NULL, 'e'}, - {"quiet", no_argument, NULL, 'q'}, - {"assume_yes", no_argument, NULL, 'y'}, - {"help", no_argument, &show_help, 1 }, - {"version", no_argument, &show_version, 1 }, - { NULL, 0, NULL, 0 } -}; - -class exception : public std::exception { -private: - std::string msg; - -public: - exception(const std::string &_msg): msg(_msg) {} - exception(const char *fmt, ...) - { - va_list ap; - char *_msg; - - va_start(ap, fmt); - _msg = vallocstr(fmt, ap); - va_end(ap); - - msg = string(_msg); - free(_msg); - } - - const char *what() const noexcept override - { - return msg.c_str(); - } - -}; - -void version(); -void usage(); -void curl_setup(); -void curl_cleanup(); -void usage2(int , const char *, ...) __attribute__((format (printf, 2, 3))); -void warnuser(const char *, ...) __attribute__((format (printf, 1, 2))); -char readanswer(const char *answers); -#ifdef HAVE_MAGIC_H -bool file_istext(char *filename); -#endif - -std::string stringtolower(std::string); -std::string decode_HTML_entities(std::string); -std::string kotlin_base_entry_point(std::string); - -Json::Value doAPIrequest(const std::string &); -bool readlanguages(); -bool readproblems(); -bool readcontests(); - -bool doAPIsubmit(); - -int nwarnings; - -/* Submission information */ -string contestid, langid, probid, baseurl; -vector filenames; -char *entry_point; -char *submitdir; - -/* Active contests */ -struct contest { - string id, name, shortname; -}; -vector contests; -contest mycontest; - -/* Languages */ -struct language { - string id, name; - bool entry_point_required; - vector extensions; -}; -vector languages; -language mylanguage; - -/* Problems */ -struct problem { - string id, label, name; -}; -vector problems; -problem myproblem; - -CURL *handle; -char curlerrormsg[CURL_ERROR_SIZE]; - -int main(int argc, char **argv) -{ - size_t i,j; - int c; - char *ptr; - char *homedir; - struct stat fstats; - string filebase, fileext; - time_t fileage; - - progname = argv[0]; - stdlog = NULL; - - if ( getenv("HOME")==NULL ) error(0,"environment variable `HOME' not set"); - homedir = getenv("HOME"); - - /* Check for USERDIR and create it if nessary */ - submitdir = allocstr("%s/%s",homedir,USERDIR); - if ( stat(submitdir,&fstats)!=0 ) { - if ( mkdir(submitdir,USERPERMDIR)!=0 ) { - error(errno,"creating directory `%s'",submitdir); - } - } else { - if ( ! S_ISDIR(fstats.st_mode) ) { - error(0,"`%s' is not a directory",submitdir); - } - if ( chmod(submitdir,USERPERMDIR)!=0 ) { - error(errno,"setting permissions on `%s'",submitdir); - } - } - - /* Set logging levels & open logfile */ - verbose = LOG_NOTICE; - loglevel = LOG_DEBUG; - - logfile = allocstr("%s/submit.log",submitdir); - stdlog = fopen(logfile,"a"); - if ( stdlog==NULL ) error(errno,"cannot open logfile `%s'",logfile); - - curl_setup(); - - logmsg(LOG_INFO,"started"); - - /* Read default for baseurl and contest from environment */ - baseurl = string(BASEURL); - contestid = ""; - if ( getenv("SUBMITBASEURL")!=NULL ) baseurl = string(getenv("SUBMITBASEURL")); - if ( getenv("SUBMITCONTEST")!=NULL ) contestid = string(getenv("SUBMITCONTEST")); - - quiet = show_help = show_version = 0; - opterr = 0; - while ( (c = getopt_long(argc,argv,"p:l:u:c:e:v::qy",long_opts,NULL))!=-1 ) { - logmsg(LOG_DEBUG, "read option `%c' with argument `%s'", - c, ( optarg==NULL ? "NULL" : optarg )); - switch ( c ) { - case 0: /* long-only option */ - break; - - case 'p': probid = string(optarg); break; - case 'l': langid = string(optarg); break; - case 'u': baseurl = string(optarg); break; - case 'c': contestid = string(optarg); break; - case 'e': entry_point = strdup(optarg); break; - - case 'v': /* verbose option */ - if ( optarg!=NULL ) { - verbose = strtol(optarg,&ptr,10); - if ( *ptr!=0 || verbose<0 ) { - usage2(0,"invalid verbosity specified: `%s'",optarg); - } - } else { - verbose++; - } - break; - case 'q': /* quiet option */ - verbose = LOG_ERR; - quiet = 1; - break; - case 'y': /* assume_yes option */ - assume_yes = 1; - break; - case ':': /* getopt error */ - case '?': - usage2(0,"unknown option or missing argument `%c'",optopt); - break; - default: - error(0,"getopt returned character code `%c' ??",c); - } - } - - logmsg(LOG_INFO,"set verbosity to %d", verbose); - - if ( show_version ) version(PROGRAM,VERSION); - - /* Make sure that baseurl terminates with a '/' for later concatenation. */ - if ( !baseurl.empty() && baseurl[baseurl.length()-1]!='/' ) baseurl += '/'; - - if ( !readcontests() ) warning(0,"could not obtain active contests"); - - if ( contestid.empty() ) { - if ( contests.size()==0 ) { - warnuser("no active contests found (and no contest specified)"); - } - if ( contests.size()==1 ) { - mycontest = contests[0]; - } - if ( contests.size()>1 ) { - warnuser("multiple active contests found, please specify one"); - } - } else { - contestid = stringtolower(contestid); - for(i=0; iWARN_MTIME ) { - warnuser("`%s' has not been modified for %d minutes",ptr,(int)fileage); - } - -#ifdef HAVE_MAGIC_H - if ( !file_istext(ptr) ) warnuser("`%s' is detected as binary/data",ptr); -#endif - - filenames.push_back(ptr); - - nextfile: ; - } - - /* Try to parse problem and language from first filename */ - filebase = string(gnu_basename(filenames[0].c_str())); - if ( filebase.find('.')!=string::npos ) { - fileext = filebase.substr(filebase.rfind('.')+1); - filebase.erase(filebase.find('.')); - - if ( probid.empty() ) probid = filebase; - if ( langid.empty() ) langid = fileext; - } - - /* Check for languages matching file extension */ - langid = stringtolower(langid); - for(i=0; i0 ) printf("There are warnings for this submission!\a\n"); - printf("Do you want to continue? (y/n) "); - c = readanswer("yn"); - printf("\n"); - if ( c=='n' ) error(0,"submission aborted by user"); - } - - doAPIsubmit(); - return 0; -} - -/* Helper function for using libcurl in doAPIsubmit() and doAPIrequest() */ -size_t writesstream(void *ptr, size_t size, size_t nmemb, void *sptr) -{ - stringstream *s = (stringstream *) sptr; - - *s << string((char *)ptr,size*nmemb); - - return size*nmemb; -} - -void curl_setup() -{ - handle = curl_easy_init(); - if ( handle == NULL ) { - error(0,"curl_easy_init() error"); - } - - curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, writesstream); - curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, curlerrormsg); - curl_easy_setopt(handle, CURLOPT_FAILONERROR, 0); - curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION,1); - curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 10); - curl_easy_setopt(handle, CURLOPT_NETRC, CURL_NETRC_OPTIONAL); - curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout_secs); - curl_easy_setopt(handle, CURLOPT_USERAGENT, DOMJUDGE_PROGRAM " (" PROGRAM " using cURL)"); - - if ( verbose >= LOG_DEBUG ) { - curl_easy_setopt(handle, CURLOPT_VERBOSE, 1); - } else { - curl_easy_setopt(handle, CURLOPT_NOPROGRESS,1); - } -} - -void curl_cleanup() -{ - // Passing a null handle is a no-op, so this is safe. - curl_easy_cleanup(handle); -} - - -void usage() -{ - size_t i,j; - - printf("Usage: %s [OPTION]... FILENAME...\n",progname); - printf( -"Submit a solution for a problem.\n" -"\n" -"Options (see below for more information):\n" -" -c --contest=CONTEST submit for contest with ID or short name CONTEST.\n" -" Defaults to the value of the\n" -" environment variable 'SUBMITCONTEST'.\n" -" Mandatory when more than one contest is active.\n" -" -p, --problem=PROBLEM submit for problem with ID or label PROBLEM\n" -" -l, --language=LANGUAGE submit in language with ID LANGUAGE\n" -" -e, --entry_point=ENTRY_POINT set an explicit entry_point, e.g. the java main class\n" -" -v, --verbose[=LEVEL] increase verbosity or set to LEVEL, where LEVEL\n" -" must be numerically specified as in 'syslog.h'\n" -" defaults to LOG_INFO without argument\n" -" -q, --quiet suppress warning/info messages, set verbosity=LOG_ERR\n" -" -y, --assume-yes suppress user input and assume yes\n" -" --help display this help and exit\n" -" --version output version information and exit\n" -"\n" -"The following option(s) should not be necessary for normal use:\n" -" -u, --url=URL submit to server with base address URL\n" -"\n" -"Explanation of submission options:\n" -"\n"); - if ( contests.size()<=1 ) { - printf( -"For CONTEST use the ID or short name as shown in the top-right contest\n" -"selection box in the webinterface.\n"); - if ( contests.size()==1 ) { - printf("Currently this defaults to the only active contest '%s'.\n", - contests[0].shortname.c_str()); - } - printf("\n"); - } - if ( contests.size()>=2 ) { - printf( -"For CONTEST use one of the following:\n"); - for(i=0; i HTML_entities = { - {"&", "&"}, - {""", "\""}, - {"'", "'"}, - {"<", "<"}, - {">", ">"}, -}; - -std::string decode_HTML_entities(std::string str) -{ - for(size_t i=0; i= 300 ) { - while ( getline(curloutput,line) ) { - printf("%s\n", decode_HTML_entities(line).c_str()); - } - if ( http_code == 401 ) { - throw ::exception("authentication failed, please check your DOMjudge credentials."); - } else { - throw ::exception("API request %s failed (code %li)", funcname.c_str(), http_code); - } - } - - logmsg(LOG_DEBUG,"API call '%s' returned:\n%s\n",funcname.c_str(),curloutput.str().c_str()); - - if ( !reader.parse(curloutput, result) ) { - throw ::exception("parsing REST API output: %s", reader.getFormattedErrorMessages().c_str()); - } - - return result; -} - -bool readlanguages() -{ - Json::Value res, exts; - - string endpoint = "contests/" + mycontest.id + "/languages"; - try { - res = doAPIrequest(endpoint); - } catch ( std::exception &e ) { - warning(0, "%s", e.what()); - return false; - } - - if (!res.isArray()) { - warning(0, "REST API returned unexpected JSON data for endpoint languages"); - return false; - } - - for(Json::ArrayIndex i=0; i::iterator last = unique(lang.extensions.begin(),lang.extensions.end()); - lang.extensions.erase(last, lang.extensions.end()); - - languages.push_back(lang); - } - - logmsg(LOG_INFO,"read %d languages from the API",(int)languages.size()); - - return true; -} - -bool readproblems() -{ - Json::Value res; - - string endpoint = "contests/" + mycontest.id + "/problems"; - try { - res = doAPIrequest(endpoint); - } catch ( std::exception &e ) { - warning(0, "%s", e.what()); - return false; - } - - if(!res.isArray()) { - warning(0, "REST API returned unexpected JSON data for endpoint problems"); - return false; - } - - for(Json::ArrayIndex i=0; i= 300 ) { - while ( getline(curloutput,line) ) { - printf("%s\n", decode_HTML_entities(line).c_str()); - } - if ( http_code == 401 ) { - error(0, "Authentication failed. Please check your DOMjudge credentials."); - } else { - error(0, "Submission failed (code %li)", http_code); - } - } - - // We got a successful HTTP response. It worked. - // But check that we indeed received a submission ID. - if ( !reader.parse(curloutput, root) ) { - error(0,"parsing REST API output: %s", - reader.getFormattedErrorMessages().c_str()); - } - - if ( !root.isObject() || !root.isMember("id") || !root["id"].isString() ) { - error(0,"REST API returned unexpected JSON data"); - } - - logmsg(LOG_NOTICE,"Submission received, id = s%s", root["id"].asCString()); - - return true; -} - -// vim:ts=4:sw=4: diff --git a/submit/submit_online.bats b/submit/submit_online.bats index f747b5c5d2..5040cad06a 100755 --- a/submit/submit_online.bats +++ b/submit/submit_online.bats @@ -1,10 +1,11 @@ #!/usr/bin/env bats # These tests assume presence of a running DOMjudge instance at the -# compiled-in baseurl that has the DOMjudge example data loaded. +# baseurl specified in the `paths.mk` file one directory up. setup() { export SUBMITCONTEST="demo" + export SUBMITBASEURL="$(grep '^BASEURL' ../paths.mk | tr -s ' ' | cut -d ' ' -f 3)" } @test "contest via parameter overrides environment" { @@ -24,7 +25,7 @@ setup() { @test "languages and extensions are in help output" { run ./submit --help echo $output | grep "C: *c" - echo $output | grep "C++: *cpp, cc, cxx, c++" + echo $output | grep "C++: *c++, cc, cpp, cxx" echo $output | grep "Java: *java" } @@ -41,7 +42,7 @@ setup() { } @test "binary file emits warning" { - cp ./submit $BATS_TMPDIR/binary.c + cp $(which bash) $BATS_TMPDIR/binary.c run ./submit -p hello $BATS_TMPDIR/binary.c <<< "n" echo $output | grep "binary.c' is detected as binary/data!" } @@ -55,17 +56,17 @@ setup() { @test "detect problem name and language" { cp ../tests/test-hello.java $BATS_TMPDIR/hello.java run ./submit $BATS_TMPDIR/hello.java <<< "n" - [ "${lines[1]}" = "Submission information:" ] - [ "${lines[4]}" = " problem: hello" ] - [ "${lines[5]}" = " language: Java" ] + [ "${lines[0]}" = "Submission information:" ] + [ "${lines[3]}" = " problem: hello" ] + [ "${lines[4]}" = " language: Java" ] } @test "options override detection of problem name and language" { cp ../tests/test-hello.java $BATS_TMPDIR/hello.java run ./submit -p boolfind -l cpp $BATS_TMPDIR/hello.java <<< "n" - [ "${lines[1]}" = "Submission information:" ] - [ "${lines[4]}" = " problem: boolfind" ] - [ "${lines[5]}" = " language: C++" ] + [ "${lines[0]}" = "Submission information:" ] + [ "${lines[3]}" = " problem: boolfind" ] + [ "${lines[4]}" = " language: C++" ] } @test "non existing problem name emits erorr" { @@ -114,13 +115,13 @@ setup() { @test "accept multiple files" { cp ../tests/test-hello.java ../tests/test-classname.java ../tests/test-package.java $BATS_TMPDIR/ run ./submit -p hello $BATS_TMPDIR/test-*.java <<< "n" - [ "${lines[2]}" = " filenames: $BATS_TMPDIR/test-classname.java $BATS_TMPDIR/test-hello.java $BATS_TMPDIR/test-package.java" ] + [ "${lines[1]}" = " filenames: $BATS_TMPDIR/test-classname.java $BATS_TMPDIR/test-hello.java $BATS_TMPDIR/test-package.java" ] } @test "deduplicate multiple files" { cp ../tests/test-hello.java ../tests/test-package.java $BATS_TMPDIR/ run ./submit -p hello $BATS_TMPDIR/test-hello.java $BATS_TMPDIR/test-hello.java $BATS_TMPDIR/test-package.java <<< "n" - [ "${lines[2]}" = " filenames: $BATS_TMPDIR/test-hello.java $BATS_TMPDIR/test-package.java" ] + [ "${lines[1]}" = " filenames: $BATS_TMPDIR/test-hello.java $BATS_TMPDIR/test-package.java" ] } @test "submit solution" { diff --git a/submit/submit_standalone.bats b/submit/submit_standalone.bats index 758d09d97c..b0d7788728 100755 --- a/submit/submit_standalone.bats +++ b/submit/submit_standalone.bats @@ -10,33 +10,37 @@ } setup() { - export SUBMITBASEURL="https://domjudge.example.org/somejudge" + export SUBMITBASEHOST="domjudge.example.org" + export SUBMITBASEURL="https://${SUBMITBASEHOST}/somejudge" } @test "baseurl set in environment" { run ./submit - echo $output | grep -E "warning: '$SUBMITBASEURL/api(/.*)?/contests': Could not resolve host" + echo $output | grep -E "$SUBMITBASEHOST.*/api(/.*)?/contests.*: \[Errno -2\] Name or service not known" [ "$status" -eq 1 ] } @test "baseurl via parameter overrides environment" { run ./submit --url https://domjudge.example.edu - echo $output | grep -E "warning: 'https://domjudge.example.edu/api(/.*)?/contests': Could not resolve host" + echo $output | grep -E "domjudge.example.edu.*/api(/.*)?/contests.*: \[Errno -2\] Name or service not known" run ./submit -u https://domjudge3.example.edu - echo $output | grep -E "warning: 'https://domjudge3.example.edu/api(/.*)?/contests': Could not resolve host" + echo $output | grep -E "domjudge3.example.edu.*/api(/.*)?/contests.*: \[Errno -2\] Name or service not known" [ "$status" -eq 1 ] } @test "baseurl can end in slash" { run ./submit --url https://domjudge.example.edu/domjudge/ - echo $output | grep -E "warning: 'https://domjudge.example.edu/domjudge/api(/.*)?/contests': Could not resolve host" + echo $output | grep -E "domjudge.example.edu.*/api(/.*)?/contests.*: \[Errno -2\] Name or service not known" [ "$status" -eq 1 ] } @test "display basic usage information" { run ./submit --help - [ "${lines[3]}" = "Usage: ./submit [OPTION]... FILENAME..." ] - [ "${lines[4]}" = "Submit a solution for a problem." ] + [ "${lines[3]}" = "usage: submit [--version] [-h] [-c CONTEST] [-p PROBLEM] [-l LANGUAGE]" ] + [ "${lines[4]}" = " [-e ENTRY_POINT] [-v [{DEBUG,INFO,WARNING,ERROR,CRITICAL}]] [-q]" ] + [ "${lines[5]}" = " [-y] [-u URL]" ] + [ "${lines[6]}" = " [filename [filename ...]]" ] + [ "${lines[7]}" = "Submit a solution for a problem." ] [ "$status" -eq 0 ] } @@ -50,13 +54,13 @@ setup() { echo $output | grep "~/\\.netrc" } -@test "nonexistent option refers to help" { +@test "nonexistent option shows error" { run ./submit --doesnotexist - [ "${lines[1]}" = "Type './submit --help' to get help." ] - [ "$status" -eq 1 ] + [ "${lines[4]}" = "submit: error: unrecognized arguments: --doesnotexist" ] + [ "$status" -eq 2 ] } -@test "verbosity option defaults to 6" { +@test "verbosity option defaults to INFO" { run ./submit -v - echo $output | grep "set verbosity to 6" + echo $output | grep "set verbosity to INFO" } diff --git a/submit/submit_wrapper.sh b/submit/submit_wrapper.sh deleted file mode 100755 index 78f13b5938..0000000000 --- a/submit/submit_wrapper.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -# -# Wrapper script for the submit client binary to pass submit url and -# possibly other information. This script should only be necessary -# when the --with-baseurl option was not specified when running configure. -# -# Use it e.g. by renaming the 'submit' client binary to 'submit-main' -# and install this script as 'submit' on the teams' workstations. -# Replace the SUBMITBASEURL variable's value with your local -# value and modify the last list to execute the correct main submit -# program. - -SUBMITBASEURL="http://localhost/domjudge/" - -export SUBMITBASEURL - -exec submit "$@" diff --git a/webapp/config/static.yaml.in b/webapp/config/static.yaml.in index 2f3e1db73d..26983977e6 100644 --- a/webapp/config/static.yaml.in +++ b/webapp/config/static.yaml.in @@ -12,4 +12,3 @@ parameters: domjudge.rundir: @domserver_rundir@ domjudge.tmpdir: @domserver_tmpdir@ domjudge.baseurl: @BASEURL@ - domjudge.submitclient_enabled: @SUBMITCLIENT_ENABLED@