Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Grzegorz 'baziorek' Bazior
Narzędzia dla programistów C++

[DOMjudge] Narzędzie do automatycznej weryfikacji zadań programistycznych

[artykuł] DOMjudge - narzędzie online-judge, do automatycznego sprawdzania zadań nadesłanych w różnych językach programowania i przeprowadzania konkursów.

DOMjudge - co to jest i przekrój możliwości

DOMjudge jest narzędziem do automatycznego sprawdzania nadesłanych zadań spisanych w różnych językach programowania. Ma otwarte źródła i wciąż jest intensywnie rozwijany (co widać po commitach), dlatego był już używany w wielu konkursach programistycznych, których lista jest na stronie domowej.

Możliwości

Narzędzie dostarcza bogatego interfejsu dla użytkowników biorących udział w konkursach, administratorów i sędziów. W konkursach programistycznych cenną jest możliwość pracy w zespołach, w których to dowolny członek może wysłać rozwiązanie liczące się jako rozwiązanie zespołowe. Wyniki pracy zespołów można ze sobą porównywać w wygodnych tabelkach.
Z punktu widzenia testowania nadesłanych rozwiązań istnieje możliwość modyfikacji sposobu budowania plików w określonych językach programowania, ważne, że nadsyłane rozwiązania mogą składać się z wielu plików źrółowych. Poza testowaniem przez komunikację między testowanym programem, przy pomocy standardowego wejścia i wyjścia, istnieje możliwość napisania innych programów (niekoniecznie muszą to być skrypty), które będą testować nadesłane rozwiązania przy pomocy innych mechanizmów komunikacji międzyprocesorowej, np. potoków. Przy większym konkursie jest możliwość używania wielu maszyn do weryfikacji nadsyłanych rozwiązań. Do testowanego rozwiązania można narzucić limity na czas wykonywania, zużycie pamięci i inne limity podczas kompilacji i wykonywania.
Olbrzymią zaletą narzędzia jest dobra dokumentacja z dużą ilością dostępnych przykładów. Jest ona podzielona na dokumentację dla administratorów, użytkowników i sędziów.
Aby korzystać z narzędzia wystarczy dostęp przy pomocy przeglądarki przez protokoły HTTP/HTTPS, jest również dostępne api dla tych protokołów.
Zalet i możliwości tego narzędzia jest znacznie więcej, wiele z nich, Drogi Czytelniku, zauważysz czytając niniejszy artykuł.

Wady

Oczywiście narzędzie to, jak wszystko co piszą informatycy, ma pewne wady, znalazłem m.in:
  • brak możliwości, w łatwy sposób, aby nadsyłane były jedynie części programów, które to zostałyby skonsolidowane w całość z dostarczoymi testami w tym samym języku programowania (co prawda w ramach niniejszego artykułu pokażę jak to zrobić)
  • dla niektórych może być utrudnieniem fakt, że zadanie wysłane przez kogokolwiek z drużyny jest jako wysłane przez daną drużynę, a nie idzie na konto wysyłającego.
  • Nie jest też możliwe z poziomu interfejsu webowego ani API utworzenie wielu kont wielu użytkownikom w łatwy sposób, trzeba albo zrobić to ręcznie, albo dać możliwość samodzielnej rejestracji.

Zanim zaczniesz czytać dalej

Rozbudowane narzędzie wymaga niestety trochę czasu, aby je zrozumieć, dlatego też, Drogi Czytelniku, przeanalizuj wady (i zalety) jakie wymieniłem powyżej. Przeanalizuj także poniższe screeny z interfejsu różnych typów użytkowników i ponawiguj sobie po przykładowych interfejsach interaktywnych dostępnych on-line. Zastanów się do jakich zastosowań potrzebujesz takiego narzędzia i czy nie lepiej napisać własnego.
Jeśli jeszcze Cię nie zniechęciłem, to dobrze.

Interfejs graficzny przez przeglądarkę

Poniżej zamieściłem parę przykładowych screenów widzianych przez przeglądarkę przy pracy z narzędziem DOMjudge:
Przykładowy interfejs bez zalogowania
Przykładowy interfejs bez zalogowania

Patrząc na komórkę pod danym zadaniem programistycznym mamy dwie liczby. Oznaczają one (ilość wysyłek zanim dane rozwiązanie okazało się poprawne) / (ilość minut które upłynęły od pojawieniu się problemu programistycznego do czasu wysłania poprawnego rozwiązania).

Interfejs zalogowanego użytkownika biorącego udział w nadsyłaniu rozwiązań
Interfejs zalogowanego użytkownika biorącego udział w nadsyłaniu rozwiązań

Jak widać możliwe jest wysyłanie rozwiązań problemu w wielu językach programowania, jest historia nadesłanych rozwiązań oraz wyniki testów automatycznych dla poszczególnych wysłanych zadań.

Strona główna interfejsu administratora
Strona główna interfejsu administratora
Na tej stronie widać przekrój możliwości administratora DOMjudge, a jak wiadomo administrator może wszystko.
Przydatnym jest na stronie głównej dostępność dokumentacji.

Podgląd, dodawanie i edycja zadań programistycznych w interfejsie administratora
Podgląd, dodawanie i edycja zadań programistycznych w interfejsie administratora
Jak widać każdy z problemów może być dostępny w wielu konkursach.

Podgląd konkretnego zadania programistycznego z poziomu administratora
Podgląd konkretnego zadania programistycznego z poziomu administratora
Jak widać każdy z problemów ma swój tekst dostępny dla użytkowników. Widać też ustawienia dostępne dla danego problemu, w tym skrypty służące do uruchamiania i weryfikowania rezultatu po wykonaniu programu. Ustawienia te można dowolnie zmieniać.
Widać w jakich konkursach jest widoczne dane zadanie programistyczne, a także widać jego ostatnio wysłane rozwiązania.

Podgląd nadesłanych rozwiązań w interfejsie administratora
Podgląd nadesłanych rozwiązań w interfejsie administratora

Jak widać poza podglądem nadesłanych rozwiązań, każdy z sędziów może się przypisać do danego rozwiązania, aby je dodatkowo zweryfikować ręcznie. Po wejściu w nadesłane rozwiązanie można podejrzeć kod źródłowy, wraz z kolorowaniem składni, zmianami od ostatniej wersji dokonanymi przez dany zespół, można zatwierdzić dane rozwiązanie. Sędzia i administrator mogą zarządzić ponowne przeprowadzenie testów automatycznych, można również zostawić komentarz w zadaniu, jak i odpowiedzieć na uwagi od użytkownika.

Interfejs interaktywny w realu

Na oficjalnej stronie jest możliwość przeklikania rzeczywistych  interfejsów narzędzia, wypełnionych przez przykładowe dane (link):

Plan na niniejszy artykuł

W niniejszym artykule skupię się na tym jak używać tego narzędzia od strony zarówno administratorów i sędziów, jak i uczestników konkursu. Będą przykłady używania narzędzia wraz z przykładowymi zadaniami. Będę się skupiał na zadaniach napisanych w C++, aczkolwiek narzędzie domyślnie wspiera wiele języków programowania.

Instalacja DomJudge

Aby zainstalować to narzędzie potrzebujemy system Linuxowy, najwygodniej będzie nam jak będzie to system oparty na Debianie np. Mint lub Ubuntu, gdyż w dokumentacji mamy gotowe komendy pod instalacje wielu zależności. Z istotnych zależności potrzebujemy:
Są jeszcze pewne opcjonalne zależności, opcjonalne wg dokumentacji, ale warto je zainstalować:
Szczegóły odnośnie konfiguracji i instalacji narzędzia polecam przeczytać z dokumentacji dla administratorów (polecam wersję PDF).

Szybka instalacja dla ustawień domyślnych

Poniżej szybka instalacja dla ustawień domyślnych. Przeprowadzałem to na systemi Ubuntu 18.04, dlatego dla tego systemu zamieszczam komendy. Jeśli u kogoś będzie brakować pewnych narzędzi w repozytorium należy dodać odpowiednie repozytoria lub doinstalować ręcznie.

Instalacja zależności dla DOMjudge

Po wpisaniu poniższych komend będzie trochę interakcji z użytkownikiem, warto wtedy zapamiętać hasło dla php-myadmin:
sudo apt-get install gcc g++ make zip unzip mysql-server apache2 php php-cli libapache2-mod-php php-gd php-curl php-mysql php-json php-zip php-mcrypt php-gmp php-xml php-mbstring bsdmainutils ntp phpmyadmin libcgroup-dev linuxdoc-tools linuxdoc-tools-text groff texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended texlive-lang-european

Instalacja zależności dla submit-clienta

To jest przydatne jedynie gdy chcemy z takiego narzędzia korzystać, lub przygotować je dla użytkowników-uczestników.
Gdy chcemy je zbudować dla uczestników, najlepiej się upewnić, że instalujemy dependencje w wersji statycznej, w przeciwnym razie wystarczy nam komenda:
sudo apt install libcurl4-gnutls-dev libjsoncpp-dev libmagic-dev

Instalacja zależności dla C++

sudo apt install libcurl4-gnutls-dev libjsoncpp-dev libmagic-dev make sudo debootstrap libcgroup-dev php-cli php-curl php-json php-zip php-dev procps gcc g++

Budowanie DOMjudge

Najpierw należy pobrać najnowszą wersję, możemy to zrobić z oficjalnej strony, następnie należy to rozpakować na systemie linuxowym, adres wnętrza rozpakowanego katalogu będę określał
$DOMJUDGE
.
cd $DOMJUDGE
./configure --with-baseurl=localhost/domjudge
make all
sudo make install-domserver install-judgehost install-docs
Oczywiście, jeśli chcemy na danej maszynie możemy zbudować tylko serwer, albo tylko judgehost.

Konfiguracja bazy danych (MySQL)

Root w mysqlu nie ma domyślnie hasła, warto je ustawić:
sudo mysqladmin -u root password ukryteHaslo

Możemy sprawdzić czy się nam uda wejść do bazy przy użyciu danego hasła:
sudo mysql -u root -p

Mając to możemy skonfigurować bazę danych:
sudo $DOMJUDGE/sql/dj_setup_database -u root -p ukryteHaslo install

Konfiguracja serwera Apache

cd $DOMJUDGE
sudo cp etc/apache.conf /etc/apache2/conf-available/domjudge.conf
sudo service apache2 reload && sudo a2enconf domjudge && sudo apache2ctl graceful

Konfiguracja procesu automatycznie sprawdzającego (judgedaemon)

sudo useradd -d /nonexistent -U -M -s /bin/false domjudge-run
sudo groupadd domjudge-run

sudo cp $DOMJUDGE/etc/sudoers-domjudge /etc/sudoers.d/

sudo sed -i  's/^GRUB_CMDLINE_LINUX_DEFAULT=.*$/GRUB_CMDLINE_LINUX_DEFAULT="quiet cgroup_enable=memory swapaccount=1"/g'  /etc/default/grub
sudo update-grub

sudo $DOMJUDGE/misc-tools/create_cgroups
sudo $DOMJUDGE/misc-tools/dj_make_chroot

Opcjonalne narzędzie do porównywania między wersjami wysłanych rozwiązań -xdiff php extention

Musimy wpierw zainstalować bibliotekę xfiff, następnie możemy zainstalować:
sudo pecl install xdiff
, pozostaje jeszcze dodać informacje o tym do php.ini, którego położenie można poznać wpisując
php --ini

Proces automatycznie sprawdzający czy przyszły nowe rozwiązania

Po zainstalowaniu i skonfigurowaniu narzędzia trzeba jeszcze uruchomić proces automatycznie testujący wysłane rozwiązania, jeśli mamy go zainstalowanego w domyślnej lokalizacji robimy to następującą komendą:
sudo /usr/local/bin/judgedaemon

Po jej wywołaniu w konsoli będą się pojawiały komunikaty na standardowe wyjście po pojawieniu się nowych rozwiązań.

Opcjonalne połączonie HTTPS

Jest to możliwe, najpierw trzeba skonfigurować SSL w serwerze Apache np. wg tego opisu. Następnie powinniśmy ustawić judgedaemon kopiując certyfikat i dokonując rehashowania w taki sposób:
sudo cp /etc/apache2/ssl/apache.crt /etc/ssl/certs/
sudo c_rehash
Od teraz możemy używać naszego narzędzia przez HTTPS. Oczywiście jeśli sami sobie wygenerowaliśmy certyfikat to przeglądarki będą wszystkich ostrzegać przed naszą stroną.

Sprawdzenie czy wszystko działa

Mając już w pełnu skonfigurowane DOMjuudge możemy już normalnie używać tego narzędzia, domyślnym loginem i hasłem jest
admin
/
admin
, hasło zmieniamy wchodząc do zakładki users, interfejs do zmiany hasła jest dość prosty, chociaż szczegóły o zmianie hasła opisane są później.
Jeśli ustawiliśmy adres
localhost/domjudge/
 to mamy dostępne następujące podstrony:
  • localhost/domjudge/
     - strona z wynikami drużyn, umożliwiająca zalogowanie/zarejestrowanie uczestnikom (możliwość rejestracji domyślnie wyłączona)
  • localhost/domjudge/team/
    -jw., na tę stronę automatycznie wchodzimy po zalogowaniu jako uczestnik
  • localhost/domjudge/jury/
    -strona logowania dla użytkowników-sędziów oraz administratorów
  • localhost/domjudge/api/
    -strona z listą komend dla API oraz instrukcją jak z niego korzystać
Możemy też wejść jako administrator do home->Config checker celem sprawdzenia czy nam wszystko działa, domyślnie mogą być pewne warningi, których możemy się pozbyć zmieniając
php.ini
 i ustawienia bazy danych, lub zignorować.

Opis ikon, zanim pokażę jak dodać przykładowe zadanie

Zacznę od tego, że wiele powszechnie stosowanych funkcji jest ukrytych pod przyciskami-ikonami, odpowiednio służącymi do dodawania, zapisywania na dysk, edycji i usuwania:
Przyciski-ikony do zarządzania elementami
Przyciski-ikony do zarządzania elementami
Przy pomocy tych ikon można operować na wszystkim, do czego takie opcje są przydatne. Przycisk dodawania często znajduje się pod listą elementów (np. konkursów, drużyn, zadań programistycznych itp.), natomiast pozostałe ikony znajdują się obok każdego elementu. Bardzo ciekawym jest ikona zapisu na dysku, gdyż stanowi pewnego rodzaju kopię zapasową, którą możemy potem załadować.

Dodawanie własnego zadania programistycznego

Aby dodać zadanie programistyczne, gdy wystarczy nam działanie domyślne, nie potrzeba nic wcześniej ustawiać. Poza tym zadanie po dodaniu można normalnie edytować.

Jak dodać zadanie

Aby dodać zadanie do zrobienia należy wejść do zakładki problems, następnie kliknąć przycisk dodawania nowego problemu, pojawi się nam wtedy okienko:
Dodawanie nowego zadania programistycznego
Dodawanie nowego zadania programistycznego
Mamy tam następujące możliwości ustawień:
  • nazwa problemu -tylko znaki alfanumeryczne bez spacji
  • limit czasu wykonywania programu
  • opcjonalny limit zużytej pamięci RAM -tutaj bardzo proszę uważać
  • opcjonalny limit wydruku na ekran
  • tekst opisujący, co jest do zrobienia, może to być .pdf, .txt czy .html
  • wybrane z listy narzędzie do uruchamiania
  • wybrane z listy narzędzie do porównywania
  • opcjonalnie argumenty skryptu porównującego
Bardziej szczegółowo o narzędziach do uruchamiania nadesłanego rozwiązania, po ewentualnym skompilowaniu oraz porównywania, informacje znajdują się poniżej.
Treści domyślnych narzędzi/skryptów budujących i testujących znajdują się po rozpakowaniu DOMjudge w katalogu
{domjudge}/sql/files/defaultdata/
, poza tym można je podejrzeć wchodząc do executables.
Należy uważać z limitem zużytej pamięci RAM, gdyż jeśli program go przekroczy pojawi się bardzo nieintuicyjna informacja-błąd wykonywania z programem przerwanym signałem 9:
Podgląd wysłanego rozwiązania danego problemu programistycznego, wraz z rezultatem automatycznego sprawdzania
Podgląd wysłanego rozwiązania danego problemu programistycznego, wraz z rezultatem automatycznego sprawdzania
Możemy jeszcze załadować zadanie programistyczne przez archiwum ZIP. Tego typu opcja jest przydatna, gdy chcemy przenieść ustawienia z innego serwera. W innych przypadkach jest to trochę niewygodne, gdyż paczka musi spełnić wymagania paczki z problemami Kattis, szczegółowy opis jakie pola musimy wypełnić znajduje się w dokumentacji dla sędziów w rozdziale Problem package format. Ciekawym faktem jest, iż do takiego archiwum możemy załączyć wzorcowe rozwiązanie, które od razu jest sprawdzane. Są też pewne narzędzia do veryfikacji takich archiwów.

Dodawanie przypadków testowych

Po dodaniu własnego zadania programistycznego pozostaje jedynie dodać przypadki testowe, jeśli nie dodamy chociaż jednego to w interfejsie administratora będziemy mieli sygnalizowany błąd internal error, oraz nie będziemy mogli testować naszego programu. Jeden przypadek testowy w zadaniu programistycznym to dwa pliki, z których najczęściej jeden zawiera dane na standardowe wejście testowanego rozwiązania, drugi dane oczekiwane na standardowym wyjściu programu, będące odpowiedzią na zadane standardowe wejście. Oczywiście dzięki możliwości używania własnych skryptów do testowania i porównywania możemy traktować te pliki inaczej.
Dodajemy przypadki testowe już w istniejącym zadaniu programistycznym, aby je dodać musimy więc wejść do danego problemu, kliknąć details/edit przy polu nazwanym Testcases, po kliknięciu naszym oczom pojawi się ekran podobny do:
Dodawanie plików do przypadku testowego
Dodawanie plików do przypadku testowego

Poza możliwością załadowania plików, warto jeszcze wpisać opis przypadku testowego. Możemy jeszcze zaznaczyć, czy podane w ramach tego testu dane testowe mają być "przykładowymi", czyli widocznymi dla uczestników konkursu. Ładować obrazka nie musimy. Po ustawieniu wszystkich informacji możemy zatwierdzić nasz test.
Możemy podmienić pliki z przypadkami testowymi, natomiast niestety nie możemy ich usunąć z danego testu z poziomu interfejsu administratora, dlatego lepiej nie dodawać na zapas.
Po dodaniu zadania programistycznego, aby można było przyjmować jego rozwiązania, należy go dodać do jakiegoś konkursu, ja w tym celu utworzyłem nowy konkurs, ale można też skorzystać z istniejącego.

Przykład dodawania własnego zadania

Pewien wstęp już mamy więc teraz nam pozostaje dodać nowe zadanie w zakładce problems. Niech to będzie liczenie silni, przykładowo ustawmy nazwę silnia, limit czasowy 1 sekunda, potrzebujemy jeszcze załączyć tekst: np. w formacie TXT, proponuję:
Mamy napisać program, który na standardowe wejście otrzymuje liczbę, w odpowiedzi ma na standardowe wyjście wypisać wartość silni dla otrzymanej liczby. Testowanie ma odbywać się tak długo, dopóki nie zostanie podany znak końca pliku na standardowe wejście.
Liczby są podawane w formie liczba\n, w tej samej formie oczekujemy odpowiedzi.
Zakres podawanych liczb na standardowe wejście to liczby całkowite [0, 21].
Po dodaniu zadania wchodzimy w jego edycję i jako przypadki testowe proponuję jako Input testdata: załadować plik:
1
3
0
10
20
Natomiast jako Output testdata: ładujemy:
1
6
1
3628800
2432902008176640000
polecam w tym przypadku zaznaczyć, iż jest to "przykładowy" przypadek testowy, oraz mimo wszystko dodać opis.

Przykładowe rozwiązanie silni w C++17, z naciskiem na szybkość wykonywania

Poniższe rozwiązanie zostało zaproponowane przez pewnego Eksperta C++ z niniejszego serwisu, który jednak planuje pozostać anonimowy:
C/C++
#include <cstdio>
#include <cstdint>
#include <cinttypes>
#include <array>

using T = std::uint_fast64_t;

constexpr T factorial( T n )
{
   
T result = 1;
   
while( n > 1 )
       
 result *= n--;
   
   
return result;
}

constexpr auto makeFactorials()
{
   
std::array < T, 21 > a { };
   
for( T i = 0; i < a.size(); ++i )
       
 a[ i ] = factorial( i );
   
   
return a;
}

constexpr auto factorials = makeFactorials();

int main()
{
   
T n;
   
while( std::scanf( "%" SCNuFAST64, & n ) != EOF )
   
std::printf( "%" PRIuFAST64 "\n", n < factorials.size() ? factorials[ n ]
        :
T
    { } );
}

Czego nam brakuje, aby można było automatycznie sprawdzać rozwiązania tego zadania?

Poniżej opis pewnych kroków, które należy w tym celu wykonać, ich szczegółowy opis znajduje się w dalszych częściach artykułu:
  • Zadania wykonuje się w ramach konkursów, możemy użyć istniejącego, ale lepiej utworzyć nowy. Zarządzamy konkursami wchodząc jako administrator do  home->Contests.
  • Zadania konkursowe rozwiązują użytkownicy, którzy są przypisani do poszczególnych drużyn. Drużyny może utworzyć administrator wchodząc w zakładkę teams.
  • Mając drużynę musimy dodać do nich co najmniej jednego użytkownika (wszak to użytkownicy się logują, a użytkownik bez drużyny nie może wysyłać zadań), zarządzanie użytkownikami odbywa się w zakładce users.

Dodawanie nowego konkursu

Aby dodać nowy konkurs wchodzimy na stronę główną z poziomu użytkownika-administratora, wybieramy contests, potem klikamy ikonę dodawania nowego i naszym oczom pojawi się ekran podobny do:
Dodawanie nowego konkursu
Dodawanie nowego konkursu

na nim uzupełniłem pola, które są obowiązkowe, a oto opis pewnych pól:
  • krótka nazwa -może mieć znaki jedynie alfanumeryczne angielskie
  • nazwa konkursu -mamy tutaj większą swobodę
  • czas aktywacji -kiedy dany konkurs będzie widoczny dla drużyn/publicznie
  • czas rozpoczęcia/zakończenia -czas kiedy widać punktacje i przyjmowane są rozwiązania
  • czas zamrożenia/odmrożenia -w czasie zamrożenia nie jest aktualizowana punktacja (stosuje się to często, aby zwiększyć emocje tuż przez zakończeniem konkursu)
  • czas deaktywacji -po tym czasie znika konkurs -nie ma punktów, możliwości wysyłania, zostaje jedynie w naszej pamięci
  • "balony" -czyli dodatkowy sposób przekazywania powiadomień, np. poprzez maila
  • publiczność -można tworzyć prywatne konkursy (jedynie dla niektórych drużyn) po wybraniu no możemy dodać nazwy drużyn, które będą mogły wysyłać na dany konkurs
  • aktywność -aktywny, czy nie
  • lista problemów, które wchodzą do danego konkursu. Możemy raz zrobić problem, a potem używać go wielokrotnie w ramach różnych konkursów
Po ustawieniu tego wszystkiego pozostaje jedynie skonfigurować użytkowników i drużyny. Należy jeszcze pamiętać o tym, aby co najmniej jeden proces wykonujący kompilacje był aktywny.

Tworzenie użytkowników i drużyn

Niestety do tworzenia nowego użytkownika i drużyny nie ma w interfejsie webowym możliwości utworzenia wielu użytkowników i drużyn równocześnie. Musimy każdorazowo utworzyć użytkownika, a aby ten miał możliwość wysyłania zadań musi należeć do jakiejś drużyny, oczywiście drużynę można też dodać z poziomu interfejsu webowego.

Tworzenie drużyny

Aby utworzyć drużynę wchodzimy na zakładkę teams, potem klikamy ikonkę dodania nowej drużyny i naszym oczom pojawia się interfejs:
Widok tworzenia drużyny
Widok tworzenia drużyny
Na powyższym widoku widać jakie opcje możemy ustawić dla drużyny, pozwolę sobie opisać jedynie część z nich:
  • kategoria drużyny, możemy dodawać administratorów, sędziów, uczestników, obserwatorów, możemy też modyfikować listę dostępnych kategorii drużyn wchodząc z panelu administratora do Team Cetegories
  • członkowie -jest to tylko opcjonalny opis członków danej drużyny
  • afiliacja -jeśli ustawimy afiliacje (przynależność) danej drużyny to możemy ją tutaj wybrać. Afliliacje dodajemy ze strony głównej administratora wybierając Team Affiliations, afiliacjami może być np. dany zespół/firma/uniwersytet w danym kraju.
  • czas na poprawkę zadania w razie wysłania z błędem
  • opcja dodania użytkownika dla danej drużyny -jest to bardzo przydatna opcja, gdyż jeśli ją zaznaczymy tworzony jest od razu użytkownik w tej drużynie. Aby nowo utworzony użytkownik mógł się zalogować musimy jeszcze wejść do ustawień użytkowników i ustawić mu hasło
  • prywatne konkursy -drużyna może mieć dostęp do konkursów niewidocznych dla wszystkich, aby dodać takie konkursy zaczynamy pisać i przeglądarka zacznie nam podpowiadać pełne ich nazwy.

Tworzenie użytkownika

W celu utworzenia użytkownika wchodzimy na zakładkę users, gdzie możemy albo kliknąć tworzenie nowego użytkownika, albo edytować już istniejącego -interfejsy obydwu funkcji są bardzo podobne. Ja utworzyłem tego użytkownika, tworząc drużynę, dlatego mój ekran edycji istniejącego użytkownika wygląda tak:
Edycja istniejącego użytkownika
Edycja istniejącego użytkownika

Na uwagę zasługuje fakt, iż użytkownik może się zalogować jedynie, jeśli ustawimy mu hasło. Ciekawe jest, że jeśli ustawimy adres IP, to logowanie użytkownika o tym adresie IP będzie następowało automatycznie, gdy będzie on wchodził na stronę z tego adresu IP.
Utworzony użytkownik-uczestnik nie może nic zmienić w swoim profilu, nawet hasła, wszystkie zmiany musi za niego zrobić administrator!

Programistyczne tworzenie użytkowników

Za dodawanie użytkowników jest odpowiedzialna funkcja PHP:
function do_register()

znajdująca się w pliku
auth.php
, jak widać hashowanie hasła w PHPa odbywa się przy użyciu kodu:
function dj_password_hash($password)
{
 return password_hash($password, PASSWORD_DEFAULT,
                      array('cost' => PASSWORD_HASH_COST));
}
stała
PASSWORD_HASH_COST
 znajduje się w pliku
domserver-config.php
, która w czasie pisania tego artykułu wynosiła
10
. Wiedząc to można napisać kod, który wstawia wiele rekordów do bazy. Aby wiedzieć, która tabela jest za to odpowiedzialna polecam dokomentację dla administratorów, rozdział Configure the contest data.

Grupowa generacja haseł użytkownikom

Jest możliwość ze strony głównej panelu administratora wygenerowania wielu losowych haseł naraz, w tym celu wchodzimy do Manage team passwords i tam losowe hasła możemy wygenerować:
  • wszystkim zespołom,
  • zespołom bez hasła,
  • sędziom, administratorom.
Opcja jest bardzo przydatna w sytuacji, gdy utworzymy wiele drużyn, tworząc od razu użytkownika w ramach drużyny.

Samodzielna rejestracja użytkowników

Ta opcja jest domyślnie wyłączona, ale jeśli wejdziemy z głównej strony panelu administratora do Configuration settings, możemy zmienić te ustawienia obok pola Allow registration, do zarejestrowania potrzeba tylko nazwa użytkownika i hasło.

Wysyłanie zadań przez użytkowników

Mając już wszystkie przygotowania zrobione, oraz potworzonych użytkowników, mogą oni wysyłać zadania, w tym celu muszą zalogować się:
http://{adres naszego domjudge}
, tam należy wybrać login, z prawej strony na górze wybieramy konkurs, na który chcemy wysłać zadanie i pojawi się nam strona:
Interfejs zalogowanego użytkownika-uczestnika
Interfejs zalogowanego użytkownika-uczestnika

możemy wybrać plik/pliki, wybieramy, którego zadania programistycznego jest to rozwiązania, a język programowania się nam ustawi automatycznie, zależnie od rozszerzenia pliku, oczywiście jeśli chcemy możemy go zmienić.
Możemy załączać równocześnie wiele plików źródłowych i nagłówkowych, ale muszą to być oddzielne pliki. Jeśli chcielibyśmy móc wrzucać archiwum, powinniśmy zmienić skrypt kompilujący tak, aby wpierw je rozpakowywał.
Jeśli się wszystko zgadza, możemy kliknąć submit. Po wysłaniu pojawi się nam poniżej informacja, że wysłaliśmy rozwiązanie problemu X, w języku CPP, a rezultat pojawi się za chwilę PENDING, nic nie musimy robić, a jeśli wszystko pójdzie dobrze, strona po chwili się automatycznie odświeży i naszym oczom powinien pokazać się wynik automatycznego testu:
Strona zalogowanego użytkownika po załadowaniu rozwiązania zadania programistycznego po automatycznym sprawdzeniu
Strona zalogowanego użytkownika po załadowaniu rozwiązania zadania programistycznego po automatycznym sprawdzeniu

widać tam ilość zdobytych punktów, pozycję w rankingu oraz ilość prób zanim dane zadanie zostało zaakceptowane łamane na ilość minut między aktywacją zadania a wysłaniem poprawnego rozwiązania, na obrazku widać dwa problemy, z których rozwiązaliśmy tylko jeden.
W czasie, gdy kompilowane są nadesłane rozwiązania, możemy podejrzeć standardowe wyjście procesu budującego, który dane zadanie zbudował (jeśli mamy wiele uruchomionych trzeba znaleźć tego, co budował) i zobaczymy m. in. jaka jest ścieżka robocza, jakie skrypty są wywoływane, co było wypisywane na ekran itp.

Znaczenie rezultatów

Warto wiedzieć, co oznaczają rezultaty i na jakim etapie dany rezultat może się pojawić, oto możliwe rezultaty.
CORRECT Rozwiązanie przeszło poprawnie wszystkie testy
COMPILER-ERROR Błąd kompilacji. Można wejść na dane zadanie i zobaczyć szczegóły kompilacji (o ile ta opcja nie będzie wyłączona)
TIMELIMIT Program wykonywał się dłużej niż maksymalny dozwolony limit, więc został ubity
RUN-ERROR Program się wywalił lub zwrócił status inny niż 0
NO-OUTPUT Brakuje wypisywania na standardowe wyjście!
OUTPUT-LIMIT Program wypisuje znacznie więcej niż pozwalają na to limity
WRONG-ANSWER Program udziela niewłaściwej odpowiedzi
TOO-LATE Wysłano po zakończeniu konkursu, nadesłane rozwiązanie nie jest procesowane
QUEUED/PENDING Wysłane rozwiązanie czeka na proces, który je sprawdzi
JUDGING Proces aktualnie sprawdza wysłane rozwiązanie, za chwilę powinien być wynik
Dodam, że zwracając odpowiednie kody błędów ze skryptów użytkownik otrzymuje dany rezultat.

Jak DOMjudge sprawdza -opis kroków

Mamy już możliwość sprawdzania rozwiązań prostego zadania, bazując na ustawieniach domyślnych, warto więc się pochylić pochylić nad sposobem działania automatycznego sprawdzania, aby wiedzieć jak ono działa.
Po krótce proces sprawdzania składa się zasadniczo z trzech kroków:
  • zbudowania nadesłanego zadania (co prawda krok ten może nic nie robić, gdy mamy do czynienia z językami interpretowanymi)
  • wykonania pliku wykonywalnego (czyli tego, co zastaliśmy po poprzednim kroku budowania)
  • przetestowanie wyników programu -oczekiwania vs rzeczywistość

Bardziej szczegółowy sposób działania DOMjudge

Bardziej szczegółowo wygląda to w taki sposób (dodam, że kolejny etap uruchomi się, jeśli poprzedni wykona się poprawnie):
  • użytkownik wysyła pliki z kodem źródłowym programów, wysłane dane oczekują, aż przejmie je któryś z wolnych procesów sprawdzających
  • proces ten dokona kompilacji na systemie linuxowym. Na uwagę zasługuje fakt, iż zostaną dodane stałe kompilacji (o ile takie są możliwe w danym języku)
    ONLINE_JUDGE
     i
    DOMJUDGE
  • następnym etapem jest testowanie skompilowanego programu (lub innego pliku wykonywalnego). Składa się ono zasadniczo z dwóch etapów: uruchomienia i weryfikacji, czy dla określonych danych program doprowadził do określonego rezultatu (domyślnie jest to sprawdzenie, czy dla określonych danych wejściowych mamy określone dane wyjściowe).

Podetapy każdego z kroków automatycznego sprawdzania

Każdy z tych trzech kroków (kompilacja, uruchamianie i sprawdzenie rezultatów) ma dwa podetapy, za które odpowiadają dwa pliki wykonywalne:
  • build - plik wykonywalny (np. skrypt), który ma za zadanie zrobienie pewnych przygotowań, po których zostanie uruchomione plik wykonywalny run, dlatego też na zakończenie etapu build musi istnieć plik run. build nie dostaje żadnych argumentów uruchomienia programu, dlatego nie jesteśmy w stanie wiele w nim zrobić, ale przydaje się np. w sytuacji gdy chcemy skompilować pewne narzędzie, które pomoże nam na etapie run.
  • run - plik wykonywalny, który wykonuje dany etap testowania nadesłanego rozwiązania, jeśli mamy obostrzenia dla danego kroku (np. czas kompilacji, czas wykonywania) to właśnie przy tym etapie mają one znaczenie.
Pliki wykonywalne build i run jeśli będą skryptami powinny zaczynać się od specjalnej, pierwszej linii komentarza mówiącej przez jaki interpreter dany skrypt ma zostać wykonany, przykładowo dla basha będzie to:
#!/bin/bash

Dodam, że wygodniej jeśli pliki te są skryptami, gdyż można je wtenczas edytować przez przeglądarkę z panelu administratora.

Zmiana domyślnych kroków budowania, uruchamiania i porównywania

Można użyć utworzone przez siebie kroki, ale jeśli potrzebujemy tylko drobnych zmian (np. wprowadzenia pewnych flag kompilacji typu
-Werror
 itp. i nie przeszkadza nam, aby taki sposób kompilacji był domyślny możemy to zmienić, w tym celu wchodzimy ze strony głównej administratora na "Executables" (przetłumaczę to jako narzędzia wykonywale). Następnie pojawiają się nam wszystkie narzędzia jakie mamy, są tam kompilatory dla różnych języków programowania, narzędzia do uruchamiania programów oraz narzędzia do porównywania wyników:
Narzędzia do wykonywania poszczególnych kroków
Narzędzia do wykonywania poszczególnych kroków

Celem zmiany klikamy na edycję wiersza, który chcemy edytować, pojawia się wtedy możliwość zmiany typu "skryptu", do wyboru:
  • kompilujący
  • uruchamiający
  • porównujący
mamy też możliwość edycji treści plików tekstowych w przeglądarce: "file contents" (widać tutaj przewagę plików-skryptów nad binarnymi). Dane narzędzie może się składać z wielu plików, każdy z tych plików możemy edytować przez przeglądarkę (pamiętając oczywiście o zatwierdzeniu zmian).
Interesującą możliwością jest dodanie wyświetlania pewnych informacji wewnątrz skryptów, jeśli zostaną one dodane do skryptów kompilujących to na konsoli procesu, który akurat się podejmie kompilacji, pojawią się te informacje. Jeśli z kolei zdecydowalibyśmy się na dodanie wyświetlania pewnych informacji w skrypcie uruchamiającym to wydruk ten nie będzie widoczny w konsoli procesu-daemona, tylko jako wydruk programu.
Wreszcie "skrypt porównujący", który domyślnie jest programem napisanym w C++ porównuje linie ze sobą i mamy możliwość skonfigurowania jego wrażliwości przez opcje:
  • "case_sensitive"
  • "space_change_sensitive"
  • "float_absolute_tolerance"
  • "float_relative_tolerance"
  • "float_tolerance"
Skrypty te nie mają dostępu do dużej ilości informacji, jedynie absolutnie konieczne do wykonania przez nich pracy, czyli np. nie dostaniemy się z poziomu skryptów do nazwy użytkownika i innych danych, które nie są nam konieczne w tym momencie.

Własny krok automatycznego sprawdzania

Przygotowanie paczki

Jak wiemy z poprzedniego punktu, aby wykonać któregokolwiek z tych kroków musimy mieć plik wykonywalny build, po jego wykonaniu mamy mieć plik wykonywalny run, dlatego też nasza paczka najczęściej będzie miała pliki build i run, poza tym paczka może zawierać jeszcze inne pliki, o niemalże dowolnych nazwach i dowolnej ilości, ale warto te pliki uwzględnić w build lub run.
Paczka taka powinna być zwykłym archiwum ZIP, które ma bezpośrednio (bez katalogów) pliki build i ewentualnie run, oraz opcjonalnie możemy załączyć również inne pliki.
Nie każde archiwum ZIP jest akceptowane przez DOMjudge, przykładowo gdy z GUI wybierzemy "Utwórz archiwum" może ono nie być poprawne, natomiast bez problemu akceptowalne jest archiwum zrobione z wiersza poleceń:
zip -r nazwaPaczki.zip run build [plik1.cc [plik2.cc [...]]]

Możliwości ustawiania własnych kroków budowy

Dzięki podziałowi na etapy sprawdzania, do każdego z etapów możemy ustawiać własne, niemalże dowolne zachowania: mamy możliwość w łatwy sposób zmienić sposób uruchamiania naszego programu, oraz jego testowania, nie tylko przez sprawdzanie standardowego wejścia i wyjścia. Przykładowo testując nasz program możemy uruchomić testowany program tworząc niemalże dowolną interakcję z wysłanym programem np. potok (przykład na to jest w przykładowych problemach dostarczonych wraz z narzędziem DOMjudge, o nazwie boolfind_run, który można pobrać po wejściu na podstronę executables). Nie możemy natomiast w łatwy sposób zmieniać sposobu budowy konkretnego zadania -tego typu ustawienia wiążą się albo ze zmianą domyślnego sposobu budowania dla plików danego języka programowania lub dodania innego sposobu budowania dla danego języka programowania oprócz dotychczasowego, ale wtedy koniecznym będzie powiadomienie użytkowników, aby załączając rozwiązanie danego problemu wybrali np. c++2, a jeśli o tym zapomną to skrypty testujące powinny to wykryć i powiadomić nieuważnego użytkownika.

Dla dociekliwych -kto wywołuje poszczególne kroki oraz z jakimi argumentami

Jeśli jesteśmy ciekawi, jakie argumenty są przesyłane do tych skryptów możemy albo je wyświetlić, albo przyjrzeć się skryptom:
compile.sh, który zajmuje się kompilacją danego rozwiązania, domyślnie będzie znajdował się tutaj:
/usr/local/lib/domjudge/judge/compile.sh
 oraz testcase_run.sh, który domyślnie będzie uruchamiał i testował rezultat nadesłanego rozwiązania, domyślnie znajdujący się
/usr/local/lib/domjudge/judge/testcase_run.sh

To jest dla bardziej wtajemniczonych, ale to najlepsze miejsce, aby zawrzeć tutaj taką informacje - jeśli na etapie uruchamiania skompilowanego programu w podkroku build utworzymy plik wykonywalny o nazwie runjury to domyślnie skrypt testcase_run.sh dokona skopiowania dodatkowych programów: runjury i runpipe.
To jest o tyle istotne, że operując na innych nazwach można się dziwić, czemu w przykładzie "wbudowanym boolfind_run" wszystko działa, a w naszym nie.
Dla jeszcze bardziej dociekliwych dodam, że nasze compile.sh i testcase_run.sh są wywoływane przez ten właśnie proces, który pobiera i buduje nasze rozwiązania (i co ciekawe jest napisany w PHP): judgedaemon, znajdujący się domyślnie tutaj:
/usr/local/bin/judgedaemon
.

Dodawanie własnego kroku budowy

Wiemy już z jakich kroków składa się proces automatycznej oceny naszych rozwiązań, czas na informacje jak dodać własny krok.
Do dodania któregokolwiek z trzech kroków (compile lub run lub compare) musimy załadować z konta administratora paczkę opisaną powyżej. Aby to zrobić wchodzimy na stronę główną panelu administratora (Home), następnie klikami Executables. Paczkę możemy wtedy załączyć przez formularz ładowania pliku widoczny na screenie (uwaga -nazwa paczki jest od razu ID, którego nie możemy zmieniać). Następnie pojawi się okno podglądu tego narzędzia, gdzie możemy zmienić nazwę, krok, w którym mają być te pliki wykonywalne odpalane (do wyboru compile, run, compare) Możemy też edytować w przeglądarce treść wszystkich plików tekstowych (dlatego skrypty mają przewagę nad binarkami). Jeśli chcielibyśmy ustawić własną nazwę i ID, to pod stroną z narzędziamy (executables) klikamy ikonę dodawania, uzupełniamy widoczne pola formularza, poza załączaniem archiwum:
Dodawanie nowego narzędzia wykonywalnego
Dodawanie nowego narzędzia wykonywalnego
zapisujemy, następnie musimy wrócić do edycji narzędzia i załadować paczkę. Z dwóch sposobów polecam jednak od razu załadować paczkę z odpowiednią nazwą.

Testowanie przez testy jednostkowe

Jako, że w tym artykule skupiamy się na C++, więc czemu mamy się ograniczać jedynie do testowania samego wejścia i wyjścia, użyjmy najpopularniejszej biblioteki do testów jednostkowych: gtest. Poniżej opiszę jak to zrobić.
W niniejszym rozdziale robię coś, czego nie da się zrobić w normalny sposób przy pomocy narzędzia DOMjudge. Nie dość, że pewne funkcjonalności będą zrobione w nieładny sposób, to jeszcze na dodatek przedstawię, gdzie trzeba interweniować w oryginalny kod narzędzia, dlatego jeśli ktoś nie lubi takich praktyk zachęcam do pominięcia tego punktu.

Szybka instalacja gtest

Aby zainstalować gtest należy go pobrać, najlepiej z oficjalnej strony, następnie wystarczy po rozpakowaniu wejść do katalogu z biblioteką i wykonać następujące komendy:
cmake .
make VERBOSE=1
sudo make install

Możliwości testowania przez testy jednostkowe

Zacznę od przeglądu przykładowych rozwiązań jak to zrobić w opisywanym narzędziu, wraz z wadami poszczególnych rozwiązań:
  • Jednym ze sposobów jest ponowne budowanie programu wraz z testami na etapie uruchamiania naszego programu (etap "run"). Niestety, jeśli ktoś zrobiłby optymalizacje kompilacji, aby zyskać na czasie wykonywania to czas kompilacji+wykonywania byłby dłuższy. Istotniejszą przeszkodą jest fakt, iż ciężko się na tym etapie dostać do plików źródłowych, trzeba by modyfikować sposób budowania, aby przenosił pliki w jakieś tajemnicze miejsce.
  • Uczestnik konkursu ma załączyć swoją implementacje wraz z naszymi testami -wydaje się to najprostrzym rozwiązaniem, ale niestety każde rozwiązanie trzeba by wtedy przejrzeć na oko, czy nie ma tam oszustwa, poza tym wszystkie testy byłyby znane, więc można by tworzyć kod przechodzący testy, co nie oznacza działający.
  • Kompilowanie oryginalnego kodu do biblioteki np. .o, którą na etapie uruchamiania ("run") połączymy z testami, niestety będzie pewien narzut na czas wykonywania z powodu linkowania.
Ostatnie rozwiązanie wydaje mi się najlepsze ze złych (bo dobrych nie ma), dlatego opiszę jak je zrobić.

Dodawanie własnego języka programowania

Co prawda nie chcemy dodawać nowego języka programowania, a jedynie dodać możliwość kompilowania kodu do biblioteki. Niestety obydwa cele sprowadzają się do tego samego, musimy wykonać te same kroki, jakie wykonalibyśmy dodając kolejny język programowania.
Zanim będziemy chcieli dodać własny język programowania warto dodać narzędzia opisane powyżej, które będą to budować to wszystko.
Co prawda zamiast dodawać drugą wersję kompilowania dla C++ moglibyśmy zmienić domyślny skrypt tak, żeby sprawdzał, czy jest dostarczona funkcja
main()
, jeśli tak to buduje do binarki, w przeciwnym razie do pliku skompilowanego *.o. Sprawdzenie, czy zawieramy funkcję main można zrobić przy użyciu ctagsów:
ctags -x --c++-types=f tmp.cc | cut -d' ' -f1 | grep main
.

Wykonywanie poszczególnego kroku na własny sposób

Powyżej znajduje się informacja, jak stworzyć paczkę z narzędziami odpalanymi do wykonania któregokolwiek z trzech kroków (compile lub run lub compare) oraz, że ma ona zawierać pliki wykonywalne build, po których wykonaniu ma istnieć plik wykonywalny run.

Własny krok kompilacji

Wróćmy do kroku kompilacji (compile) naszej biblioteki, nasz plik build nie musi nic robić:
#!/bin/sh
# nothing here to do:)

Plik run z kolei musi wykonać kompilacje:
#!/bin/bash

FLAGS='-x c++ --std=c++14 -Wall -Wextra -DONLINE_JUDGE -DDOMJUDGE'

function getCppSources
{
    for filename; do
        file_extention=${filename##*.}
        [ ${file_extention:0:1} == "c" ] && echo $filename
    done
}

DEST="$1" ; shift
MEMLIMIT="$1" ; shift
CPP_SOURCES=$(getCppSources "$@")

g++ $FLAGS -c -o ${DEST}.o ${CPP_SOURCES}
returnCode="$?"

cp "$DEST".o "$DEST"

exit ${returnCode}
Słowem wyjaśnienia:
  • W pierwszej linii specyfikujemy przez jakie narzędzie nasz plik będzie wykonywany, powyższy skrypt jest napisany w bashu, co jest uwzględnione w pierwszej linii.
  • Następnie definiujemy flagi i funkcję pomocniczą. Domyślnie narzędzie dodaje stałe czasu kompilacji ONLINE_JUDGE i DOMJUDGE, więc czemu mamy we własnym skrypcie tego nie robić?
  • Następnie pobierany argumenty uruchomienia skryptu, z których ważna jest kolejność.
  • Kompilacja, musimy pamiętać, że ma powstać plik
    $DEST
    , dlatego po utworzeniu pliku .o dokonuję kopiowania.
  • Na koniec musimy zwrócić wartość oznaczającą, czy kompilacja odbyła się bezbłędnie (wartość 0), lub z błędami (wartość różna od 0), taką informację zwróci nam kompilator.
Warto uwzględnić fakt, iż co wypiszemy na ekran w pliku build zostanie wyświetlone przez proces wykonujący automatyczne sprawdzenie, natomiast co wypiszemy w pliku run zostanie wypisane jako logi kompilacji dla użytkownika, tym samym wszystkie warningi użytkownik zobaczy.

Tworzenie paczki

Mając już przygotowane pliki możemy utworzyć archiwum zip. Nazwa archiwum jest od razu ID narzędza wykonującego dany krok, którego potem nie można zmienić, a poza tym podlega obostrzeniom dotyczącym ID (znaki alfanumeryczne bez spacji).

Można utorzyć paczkę z poziomu terminala:
zip -r cpp_lib.zip run build

po utworzeniu paczki warto wejść do jej ustawień i zmienić opis paczki (opis jest widoczny dla użytkownika).
Po zrobieniu tej paczki następuje moment, kiedy trzeba ją załadować, wchodzimy więc do executables i tam klikamy nowy, ładujemy paczkę, następnie ustawiamy opis i do czego dany skrypt służy -w naszym przypadku wybieramy compile.

Modyfikacja skryptów DomJudge, aby nam darował gdy nie utworzymy plików wykonywalnych

Niestety teraz następuje nieprzyjemny moment, kiedy musimy modyfikować oryginalny kod w narzędziu, a mianowicie ze skryptów wywołujących kompilacje usunąć sprawdzenie, czy po wywołaniu budowania powstał plik wykonywalny (w bashu opcja
-x
).

Zacznijmy od pliku
compile.sh
, który domyślnie będzie znajdował się tutaj:
/usr/local/lib/domjudge/judge/compile.sh

W pliku tym musimy usunąć bashowe sprawdzenie, czy powstał plik wykonywalny (poniżej zacytowałem kod z pliku
compile.sh
):
if [ ! -f compile/program ] || [ ! -x compile/program ]; then
 echo "Compiling failed: no executable was created; compiler output:" >compile.out
 cat compile.tmp >>compile.out
 cleanexit ${E_COMPILER_ERROR:--1}
fi
w taki sposób:
if [ ! -f compile/program ]; then
 echo "Compiling failed: no file was created; compiler output:" >compile.out
 cat compile.tmp >>compile.out
 cleanexit ${E_COMPILER_ERROR:--1}
fi

Sprawdzenie tego odbywa się również w kolejnym skrypcie:
testcase_run.sh
, znajdującym się domyślnie:
/usr/local/lib/domjudge/judge/testcase_run.sh
, z niego musimy usunąć następujące linijki:
[ -x "$WORKDIR/$PROGRAM" ] || error "submission program not found or not executable"
Usunięcie tego typu sprawdzania nie jest bez konsekwencji, gdyż te pliki dotyczą wszystkich języków programowania. Po tej zmianie pozbywamy się jednego z zabezpieczeń, które się jednak przydaje.

Dodawanie języka programowania

Mamy już dodany sposób budowania kodu do biblioteki, więc możemy przejść do dodawania własnego języka programowania. W tym celu wchodzimy z panelu administratora na języki Languages, naszym oczom ukaże się lista języków, po kliknięciu ikony dodania nowego języka zobaczymy:
Dodawanie języka programowania
Dodawanie języka programowania

Należy mieć na uwadze, że wartość wpisana w "Language name" jest widoczna dla użytkowników.

Własny sposób uruchomienia programu

Uruchomienia, a mamy na razie jedynie bibliotekę. Podobnie jak wcześniej przygotowujemy pliki run i build, oraz dodatkowo musimy dodać testy do paczki oraz inne konieczne pliki do skompilowania testów. Dla naszej silni będzie to plik z przykładowymi testami silni
factorian_test.cc
 o treści:
C/C++
#include "factorian.h"
#include <gtest/gtest.h>

using namespace::testing;


TEST( factorians, testZero )
{
   
ASSERT_EQ( 1, factorian( 0 ) );
}

TEST( factorians, testGreatNumber )
{
   
ASSERT_EQ( 2432902008176640000LLU, factorian( 20 ) );
}


int main( int argc, char * argv[ ] )
{
   
::testing::InitGoogleTest( & argc, argv );
   
GTEST_FLAG( print_time ) = false;
   
return RUN_ALL_TESTS();
}
Dlaczego stosuję tę flagę
GTEST_FLAG( print_time )
 wyjaśnię później, obecnie potrzebujemy jeszcze pliku nagłówkowego
factorian.h
, przykładowo:
C/C++
#ifndef FACTORIAN_H
#define FACTORIAN_H

#include <cstdint>

typedef std::uint_fast64_t T;

T factorian( T value );

#endif  // FACTORIAN_H
Plik build, który będzie u mnie skryptem bashowym będzie budował testy do postaci skompilowanej:
#!/bin/bash

FLAGS='-x c++ --std=c++14 -Wall -Wextra -DONLINE_JUDGE -DDOMJUDGE'

g++ $FLAGS factorian_test.cc -c -o tests.o

exit $?
Wreszcie plik run, który dokona linkowania skompilowanego kodu i testów do postaci programu, po czym go uruchomi:
#!/bin/bash

LINK='-pthread -lgtest -lgtest_main'

TESTIN="$1";  shift
PROGOUT="$1"; shift

programPath="${@: -1}" # last script argument is executable path
programPathAbsolute=$(readlink -f ..${programPath})

testLibraryAbsolutePath=$(readlink -f ../../executable/cpp_lib_run/tests.o)

g++ --std=c++14 ${testLibraryAbsolutePath} "${programPathAbsolute}".o -o "${programPathAbsolute}" ${LINK} # linking

rm "${programPathAbsolute}".o

exec "$@" < "$TESTIN" > "$PROGOUT"
Teraz parę słów komentarza odnośnie powyższego skryptu:
  • Zacznę od flag linkowania, gtest, jak to biblioteka, potrzebuje swojej flagi, poza tym domyślnie używa pthread, więc musimy również z tą biblioteką linkować.
  • Następnie pobieram argumenty, pierwszymi są poszczególne pliki z wejściem i wyjściem, co prawda wejście jako wejście nie ma w tym przypadku dużego sensu, sensowniejszym byłoby wejście potraktować jako argumenty uruchomienia programu dla gtesta np. filtrowanie tylko pewnych testów, ale nie chciałem aż tak bardzo odchodzić od konwencji na potrzeby przykładu. Nie widać tego, ale skrypt otrzymuje jeszcze bardzo dużo innych argumentów, przykładowo u mnie były to:
    sudo -n /usr/local/bin/runguard -r /usr/local/var/lib/domjudge/judgings/ubuntu/endpoint-default/c3-s26-j64/testcase001/.. --nproc=64 --no-core --streamsize=100000 --user=domjudge-run --group=domjudge-run --walltime=5:6 --cputime=5:6 --memsize=100000 --filesize=100000 --stderr=program.err --outmeta=program.meta -- /testcase001/execdir/program
     Jak widać przy uruchamianiu programu w skrypcie run otrzymujemy bardzo dużo informacji, najistotniejszy w naszym przypadku jest ostatni argument, czyli ścieżka do programu, z której musimy przejść jeden katalog wyżej (dokleić przed ścieżką do programu ../)
  • Nasze testy, mimo iż budowały się w ramach tego samego kroku -uruchamiania programu to jednak znajdują się w innym miejscu (build i run pracują na różnych ścieżkach), dlatego musimy się do nich odnieść stosując ID paczki jako nazwę katalogu (cpp_lib_run).
  • Następnie odbywa się konsolidacja testów i biblioteki użytkownika do programu do uruchomienia.
  • Potem zostanie nam tylko uruchomienie naszego programu z testami, na wejście są przekazywane dane wejściowe (mimo iż nasz program nic z nimi nie robi).
  • Wydruk z programu zostaje zapisany do konkretnego pliku.

Krok porównywania wyjścia programu z oczekiwanym wyjściem

Jako output można użyć output wygenerowany przez testy w sytuacji, gdy takowe przejdą. W naszym przypadku będzie podobny do (różnice mogą być w czasie wykonywania poszczególnych funkcji):
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from factorians
[ RUN      ] factorians.testZero
[       OK ] factorians.testZero
[ RUN      ] factorians.testGreatNumber
[       OK ] factorians.testGreatNumber
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[  PASSED  ] 2 tests.
Dzięki temu domyślny sposób porównywania wydruku z programu powinien nam wystarczyć do weryfikacji testu, aczkolwiek lepiej mieć własny do parsowania wydruku w bardziej wyrafinowanych przypadkach. Pomocna może być wtedy zmiana domyślnego sposobu wyświetlania na (do wyboru):
--gtest_output=xml
 lub
--gtest_output=json
.
Co prawda wygodnym jest stosowanie argumentów sterujących gtestem takie jak powyższe, niestety jeśli zamienilibyśmy spobób uruchamiania programu ze skryptu run z:
exec "$@" < "$TESTIN" > "$PROGOUT"
 na np.
exec "$@ --gtest_output=json" < "$TESTIN" > "$PROGOUT"
, to pojawiają się błędy z poziomu narzędzia uruchamiającego nasz program runguard, dlatego takie ustawienia należałoby niestety zrobić z poziomu wnętrza programu, przynajmniej do czasu, aż twórcy narzędzia nie naprawią tej niedogodności, która pachnie błędem.

Implementacja funkcji przy użyciu C++14 nastawiona na szybkość

Plik
#include "factorian.h"
 znajduje się w podrozdziale Własny sposób uruchomienia programu i zawiera
typedef std::uint_fast64_t T;
.
C/C++
#include "factorian.h"

constexpr T FACTORIAN_LIMIT = 22; // factorian(21) is maximum factorian to store in uint64_t

T factorianCompileTime[ FACTORIAN_LIMIT ];

template < T N >
struct Factorian
{
   
constexpr static T value = N * Factorian < N - 1 >::value;
   
   
constexpr Factorian() noexcept
   
{
       
factorianCompileTime[ N ] = value;
       
Factorian < N - 1 >();
   
}
}
;

template < >
struct Factorian < 0 >
{
   
constexpr static T value = 1;
   
   
constexpr Factorian() noexcept
   
{
       
factorianCompileTime[ 0 ] = value;
   
}
}
;

T factorian( T value )
{
   
constexpr static Factorian < FACTORIAN_LIMIT - 1 > factorianInitializer;
   
static_cast < T >( factorianInitializer ); // to avoid "unused waring"
   
   
return factorianCompileTime[ value ];
}

Funkcje dodatkowe

Zasadniczo do użycia narzędzia wystarczy to co powyżej, niemniej jednak narzędzie posiada naprawdę multum funkcji, dlatego opiszę jeszczę parę z nich.

Ustawienia zaawansowane

Ustawień zaawansowanych jest bardzo wiele, na uwagę zasługuje możliwość włączenia samodzielnej rejestracji użytkowników, ale jest wiele innych opcji, o których istnieniu warto wiedzieć. Okno ustawień widać poniżej:
Ustawienia zaawansowane z panelu administratora
Ustawienia zaawansowane z panelu administratora

Autentykacja użytkowników

Na ten problem się natknęliśmy wcześniej, ale możliwości jest znacznie więcej:
  • Ręczne tworzenie kont użytkownikom z poziomu panelu administratora, wtedy użytkownicy dostają loginy i hasła, których nie można zmienić.
  • Tworząc konta można ustawić adres IP do automatycznego logowania użytkownika wchodzącego na stronę z danego IP.
  • Używanie serwera LDAP celem logowania -może się to przydać, gdy konkurs organizujemy w firmie, w której każdy z pracowników jest już zarejestrowany.
  • Możliwość logowania przez OpenID, tę opcje należy jednak skonfigurować w panelu administratora wchodząc w home -> Configuration settings.
  • Inne metody autentykacji, w tym celu można dopisać pewną funkcjonalność w pliku
    lib/www/auth.php
    , domyślnie znajdującym się:
    /usr/local/lib/domjudge/www/auth.php

Wysyłanie zadań z poziomu wiersza poleceń i przy użyciu api

Użycie API bez autoryzacji

Mamy dostępne API pod adresem:
{serwer domjudge}/api
 w moim przypadku (domyślnie) jest to adres:
localhost/domjudge/api/

Gdy wejdziemy w powyższy link zobaczymy pełną listę dostępnych komend, możemy wysyłać przy użyciu POST i GET różne zapytania i żądania, do tego celu użyję narzędzie curl. Do pobrania pewnych informacji nie potrzebujemy uprawnień (Required role), ale są takie, do których już potrzebujemy.

Przykładowo listę konkursów i języków można pobrać następującymi komendami, poniżej których jest przykładowy output w JSONie (wszak "nie zadziała jak nie ma JSONa"):
$ curl -X GET localhost/domjudge/api/contests
{"2":{"id":2,"shortname":"demo","name":"Demo contest","start":1514804400,"freeze":null,"end":1546340400,"length":31536000,"unfreeze":null,"penalty":1200},"3":{"id":3,"shortname":"przyklad","name":"Przyk\u0142adowy konkurs","start":1531059120,"freeze":null,"end":1562616720,"length":31557600,"unfreeze":null,"penalty":1200}}

$ curl -X GET localhost/domjudge/api/languages
[{"id":"c","name":"C","extensions":["c"],"allow_judge":true,"time_factor":1},{"id":"cpp","name":"C++","extensions":["cpp","cc","c++"],"allow_judge":true,"time_factor":1},{"id":"cpp_lib","name":"cpp library","extensions":["cpp","cc","c++"],"allow_judge":true,"time_factor":1},{"id":"java","name":"Java","extensions":["java"],"allow_judge":true,"time_factor":1}]grzegorz@ubuntu:~/Pulpit/silnia_gtest$
Możliwości API jest bardzo dużo, można sprawdzać wszystko, co publicznie dostępne, zwracając jeszcze pewne dodatkowe informacje typu IDki, pełne api można podejrzeć
{serwer domjudge}/api
, a jeśli ktoś chce przed zainstalowaniem tego narzędzia, można przejrzeć i przetestować API na oficjalnej stronie w wersji interaktywnej.

Użycie API z uprawnieniami

Po zalogowaniu przez HTTP/HTTPS i mając aktywną sesję pewne dodatkowe funkcje API będą dostępne zależnie od posiadanych uprawnień. Poza tym, jeśli użytkownik ma podany adres IP w ustawieniach konta to automatycznie API dokona jego autoryzacji.
Poza tym na szczęście jest wygodniejszy sposób, gdy nie mamy ustawionego IP -utworzenie odpowiedniego pliku
~/.netrc
 o przykładowej treści:
machine adres.serwera.pl login uzytkownik password mojeHaslo

W tym pliku podajemy tylko adres serwera, czyli jak mamy przykładowo:
localhost/domjudge
 to podajemy tylko
localhost
U mnie dla użytkownika Najlepszy wygląda tak:
machine localhost login Najlepszy password pass123

Mając ten plik możemy przy pomocy curl użyć tego pliki z domyślnej (opcja
--netrc
) lub innej lokalizacji (
--netrc-file plik
):
curl --netrc -F "shortname=nazwaProblemu" -F "langid=nazwaJezykaProgramowania" -F "contest=nazwaKonkursu" -F "code[]=@plik1.cc" -F "code[]=@plik2.cc" ... http://{server}/api/submissions
Jako odpowiedź, w przypadku braku błędu, otrzymamy ID dla tego wysłania.
W moim przypadku, wraz z uwzględnieniem danych zwróconych przez api takich jak dostępne języki programowania i dostępne konkursy mogę wysłać w następujący sposób:
curl --netrc -F "shortname=silnia" -F "langid=cpp_lib" -F "contest=przyklad" -F "code[]=@factorian.cc" -F "code[]=@factorian.h" http://localhost/domjudge/api/submissions

Lepszy spobów wysyłania zadań przez konsolę -sublitclient

Stosując API do wysyłania zadań musimy niestety pobrać pewne informacje przed wysłaniem zadania, jest i na to sposób -wbudowane narzędzie do wysyłania: submitclient. Aby je zbudować musimy po pobraniu DOMjudge wywołać:
make submitclient
, następnie po wejściu do katalogu:
submit
 zobaczymy zbudowane narzędzie submit.
Możemy go użyć podając odpowiednie argumenty, których listę zobaczymy używając argumentu
--help
. Aby wysłać zadanie musimy napisać przykładowo:
./submit --url='http://localhost/domjudge/' --contest=przyklad --language=cpp --problem=silnia --verbose ~/Pulpit/silnia_gtest/factorian.h ~/Pulpit/silnia_gtest/factorian.cc
w odpowiedzi otrzymamy coś podobnego do:
[Jul 12 13:19:59.193] submit[6623]: connecting to http://localhost/domjudge/api/languages
[Jul 12 13:19:59.208] submit[6623]: connecting to http://localhost/domjudge/api/contests
Submission information:
  filenames:   /home/grzegorz/Pulpit/silnia_gtest/factorian.h /home/grzegorz/Pulpit/silnia_gtest/factorian.cc
  contest:     przyklad
  problem:     silnia
  language:    cpp library
  url:         http://localhost/domjudge/
There are warnings for this submission!
Do you want to continue? (y/n)
[Jul 12 13:20:01.605] submit[6623]: connecting to http://localhost/domjudge/api/submissions
[Jul 12 13:20:01.774] submit[6623]: Submission received, id = s43
Może być jeszcze prościej: po dodaniu ścieżki z naszym submit do zmiennej środowiskowej
PATH
 możemy pewne, stałe elementy wpisać do pliku submit_wrapper.sh, który znajduje się w tym samym katalogu, zaoszczędzimy sobie tym sposobem pisania, gdyż submit_wrapper.sh woła submit, który z kolei korzysta z API.

Porównywanie wyjścia programu przy użyciu gramatyki

Jeśli domyślny sposób porównywania standardowego wyjścia programu z oczekiwanym standardowym wyjściem nam nie odpowiada i nie chcemy do każdego z zadań pisać oddzielnego programiku do parsowania i porównywania twórcy DOMjudge napisali w innym repozytorium pomocne narzędzie do weryfikacji przy użyciu gramatyk.

Balony -alternatywny sposób powiadomień

Jeśli chcemy możemy skonfigurować alternatywny sposób powiadomień dla sędziów, może to być dowolna komenda, którą konfigurujemy w pliku
domserver-config.php
, domyślnie znajdującym się
/usr/local/etc/domjudge/domserver-config.php
, może to być np. wysyłanie maili, w tym celu zmieniamy treść stałej
define('BALLOON_CMD', '');
 wstawiając tam komendę, która ma zostać zawołana celem powiadomienia sędziów.
Poza tym musimy jeszcze uruchomić proces, który będzie się zajmował powiadomieniami, domyślnie będzie to:
/usr/local/bin/balloons

Komunikacja między uczestnikiem a sędziami

Każdy uczestnik po wysłaniu zadania może na stronie głównej kliknąć: request clarification, następnie pojawi się okienko w którym można napisać do sędziego:
Prośba o wyjaśnienie
Prośba o wyjaśnienie
Jeden z sędziów na to może odpisać dla całego zespołu lub nawet do wszystkich uczestników. Sędziowie mają możliwość przypisania się do sprawdzenia danego problemu oraz zaznaczenia, że udzielono odpowiedzi, odpowiedź pojawi się u uczestnika:
Odpowiedź od sędziego
Odpowiedź od sędziego
Nieprzeczytana wiadomość od sędziów będzie pogrubioną czcionką.

Bezpieczeństwo wykonywania

Co jeśli ktoś z uczestników konkursu wyśle zadanie zawierające przykładowo:
system( "rm -rf ~" );
? Otóż nic się nie stanie, gdyż każdy z programów jest uruchamiany w "piaskownicy" przez program
runguard.c
, którego kod można podejrzeć w katalogu
{domjudge}/judge/runguard.c
, program ten izoluje od plików systemowych. Jeśli jednak chcielibyśmy mieć jeszcze większe bezpieczeństwo, jest dla dockera utworzony specjalny obraz, dzięki któremu można całkowicie oddzielić narzędzie DOMjudge od naszego systemu.

Bibliografia