HazifeladatEllenorzoTeacher

A MathWikiből
(Változatok közti eltérés)
(manifest.json)
a (manifest.json)
 
(egy szerkesztő 39 közbeeső változata nincs mutatva)
8. sor: 8. sor:
  
 
Ide be lehet ssh-zni, scp-zni vagy a webmail-jébe belépni.
 
Ide be lehet ssh-zni, scp-zni vagy a webmail-jébe belépni.
 +
 +
== Honlap ==
 +
Mivel a <tt>hazi</tt> egy sima felhasználó a leibniz-en, van honlapja is:
 +
 +
  http://math.bme.hu/~hazi
 +
 +
Ide lehet közérdekű (publikus) infókat kitenni, de lehet jelszóval védett al-oldalakat is csinálni, mondjuk azok biztonsága elég enyhe.
 +
 +
Érdekesség, hogy vannak [http://math.bme.hu/~hazi/pulse terhelési grafikonok] is.
  
 
= Mappaszerkezet =  
 
= Mappaszerkezet =  
 
Ennek a felhasználónak a <tt>home</tt> mappájában a következőket találjuk:
 
Ennek a felhasználónak a <tt>home</tt> mappájában a következőket találjuk:
  
     ~
+
     [~]
     ├───hazijavitorendszer
+
     ├───[hazijavitorendszer]
     │  ├───HW
+
     │  ├───[HW]
     │  │  ├───feladat
+
     │  │  ├───[feladat]
 
     │  │  .
 
     │  │  .
 
     │  │  . (többi feladat)
 
     │  │  . (többi feladat)
28. sor: 37. sor:
 
     │  │  ├───...
 
     │  │  ├───...
 
     │  │  └───getsenderinfo
 
     │  │  └───getsenderinfo
     │  └───mailsend-go_1.0.6_linux-64bit.deb
+
     │  ├───Dockerfile
     ├───solution
+
     │  └───Dockerfile.test
     ├───logs
+
     ├───[solution]
     ├───archive
+
     ├───[logs]
     ├───test
+
     ├───[archive]
    │  └───(itt lényegében a felette lévőnek egy másolata van)
+
 
     ├───digest_logs.sh
 
     ├───digest_logs.sh
 +
    ├───checkpoints.sh
 +
    ├───...
 
     ├───archive.sh
 
     ├───archive.sh
    ├───Dockerfile
 
 
     └───run.sh
 
     └───run.sh
  
 
= Mit csináljunk =
 
= Mit csináljunk =
 
== Hallgatók kezelése ==
 
== Hallgatók kezelése ==
Ezt lényegében a <tt>userinfo.tsv</tt> (tab-separated-values) fájl szerkesztésével tehetjük meg.
+
Ezt lényegében egy <tt>hazijavitorendszer/HW/*.tsv</tt> (tab-separated-values) fájl szerkesztésével tehetjük meg.
  
 
Formátuma:
 
Formátuma:
48. sor: 57. sor:
 
  borbely@math.bme.hu Gábor Borbély info2,lecturer
 
  borbely@math.bme.hu Gábor Borbély info2,lecturer
  
 +
* Több ilyen fájl is lehet, ekkor a rendszer lényegében uniózza ezen fájlokat.
 
* Minden sora egy felhasználó
 
* Minden sora egy felhasználó
 
* A felhasználók az email-címükkel vannak azonosítva, a nevük csak tájékoztató jellegű.
 
* A felhasználók az email-címükkel vannak azonosítva, a nevük csak tájékoztató jellegű.
59. sor: 69. sor:
 
** de ha valaki másodszorra hallgatja az info2-t, akkor lehet két kurzusa: <tt>info2_2019,info2_2020</tt>
 
** de ha valaki másodszorra hallgatja az info2-t, akkor lehet két kurzusa: <tt>info2_2019,info2_2020</tt>
  
Ha a <tt>userinfo.tsv</tt> fájlt változtatjuk, akkor a módosítások csak akkor jutnak érvényre, ha '''újra <tt>build</tt>-eljük a <tt>docker image</tt>-et'''
+
Ha a felhasználók <tt>.tsv</tt> fájlját változtatjuk, akkor a módosítások csak akkor jutnak érvényre, ha [[#build|újra <tt>build</tt>-eljük a <tt>docker image</tt>-et]]
 
+
cd ~ && docker build -f Dockerfile -t hazicp hazijavitorendszer
+
  
 
== Feladatok felvétele ==
 
== Feladatok felvétele ==
71. sor: 79. sor:
 
* kellenek tesztek a mappában
 
* kellenek tesztek a mappában
 
** minden teszt neve '''<tt>i</tt>''' betűvel kell kezdődjön
 
** minden teszt neve '''<tt>i</tt>''' betűvel kell kezdődjön
** a tesztek formátuma feladattípustól függ. [[HazifeladatEllenorzoTeacher#feladattipus| lásd lentebb]]
+
** a tesztek formátuma feladattípustól függ. [[#Feladattípusok| lásd lentebb]]
  
 
Például a <tt>fahrenheit</tt> nevű feladat felvételéhez hozzuk létre az alábbiakat:
 
Például a <tt>fahrenheit</tt> nevű feladat felvételéhez hozzuk létre az alábbiakat:
81. sor: 89. sor:
 
     ├───i2.json
 
     ├───i2.json
 
     └───i3.json
 
     └───i3.json
 +
 +
Egy feladathoz 123-nál több teszt esetet nem adhatunk meg!
 +
 +
Ha egy új feladatot felveszünk vagy régit módosítunk, vagy kitörlünk, akkor a módosítások csak akkor jutnak érvényre, ha [[#build|újra <tt>build</tt>-eljük a <tt>docker image</tt>-et]]
  
 
=== manifest.json ===
 
=== manifest.json ===
98. sor: 110. sor:
 
* '''"deadline"''' egy sztring, ami a határidőt írja le
 
* '''"deadline"''' egy sztring, ami a határidőt írja le
 
** például <tt>"2020-02-14 01:00:00 UTC+1"</tt>
 
** például <tt>"2020-02-14 01:00:00 UTC+1"</tt>
** '''Muszáj időzónát megadni''' (UTC+1 a budapesti)
+
** '''Muszáj időzónát megadni''', ha UTC időzónában adjuk meg, akkor is tegyünk egy '''<tt>+0</tt>'''-t az időpont mögé.
 
** a python [https://dateutil.readthedocs.io/en/stable/parser.html dateutils.parser.parse] függvénye számára értelmezhető formátumban kell legyen
 
** a python [https://dateutil.readthedocs.io/en/stable/parser.html dateutils.parser.parse] függvénye számára értelmezhető formátumban kell legyen
 
** Ha nincsen egyáltalán "deadline" kulcs a szótárban, akkor bármikor be lehet küldeni a feladatot.
 
** Ha nincsen egyáltalán "deadline" kulcs a szótárban, akkor bármikor be lehet küldeni a feladatot.
116. sor: 128. sor:
 
** De egyszerűen megadhatunk üres disclaimer-t is, aminek a következménye, hogy csak üres levelet fogad el feladat.
 
** De egyszerűen megadhatunk üres disclaimer-t is, aminek a következménye, hogy csak üres levelet fogad el feladat.
 
** Ha nincsen disclaimer kulcs a szótárban, akkor a levél törzse irreleváns.
 
** Ha nincsen disclaimer kulcs a szótárban, akkor a levél törzse irreleváns.
 +
* '''"response"'''
 +
** Ezzel állíthatjuk, hogy a beküldő mit kapjon meg válaszként.
 +
** az értéke az alábbiak egyike (string-ként) vagy ezek listája:
 +
*** "description" az eredmény mellé megkapja a feladat kiírását is. A feladat leírását egy [[HazifeladatEllenorzo#Seg%C3%ADts%C3%A9g%20|speciális emaillel]] is meg lehet szerezni, szóval a beküldés után annyira nincsen szükség magára a feladatra, de érdemes ezt is beletenni a válaszemail-be.
 +
*** "score" az elért pontszám, ha lehet, akkor azt is odaírja hogy mennyiből, de az egy feladatra kapható maximális pontszám nem jól definiált.
 +
*** "tests" a kiértékelő script kimenete, ha ezt megadjuk és a kiértékelést végző kód kiírja hogy melyik teszt sikerült, akkor kvázi az elért pontszámot is elárultuk.
 +
** Ha nem adunk meg "response" mezőt a manifest-be, akkor a válasz mindent információt tartalmazni fog.
  
== feladattipus ==
+
== Feladattípusok ==
Ahhoz hogy új feladattípust
+
Egy feladat típusa határozza meg, hogy milyen programnyelvet várunk el a beküldőtől.
 +
Akkor tekinthető valami egy értelmes feladattípusnak, ha van egy olyan nevű futtatható fájl a <tt>HW</tt> mappában.
 +
Például ha programozási feladatból python3 programokat akarunk feladni, akkor kell legyen egy <tt>python3_program</tt> nevű futtatható állomány.
 +
A továbbiakban ez ellenőrzi le azt a feladatot, aminek a "type" mezőjében a "python3_program"-ot adtuk meg.
 +
 
 +
Lentebb részletezzünk, hogy milyen feladattípusok vannak és hogy melyik milyen sajátosságokkal rendelkezik.
 +
De definiálhatunk [[#definiálás|saját feladattípust]] is.
 +
 
 +
=== program ===
 +
Általában bármilyen parancssorból hívható programot tesztelhetünk ezzel. Akkor is ha interpretált vagy ha fordított nyelven van írva. Persze ehhez installálva kell legyen a szükséges fordító és/vagy interpreter a Docker image-ben.
 +
 
 +
Akkor használjunk ilyen faladattípust, ha
 +
* azt akarjuk, hogy a beküldés egy interpreter által futtatott vagy fordító által fordított fájl legyen, ami parancssorból működik
 +
 
 +
Ahhoz hogy egy ilyen programot teszteljünk, az alábbiakat kell megadnunk a <tt>manifest.json</tt> fájlban:
 +
* <tt>"type": "program"</tt>
 +
* <tt>"compile"</tt> ennek az értéke egy parancs [https://stackoverflow.com/a/47940538 docker exec] formátumban, vagy ilyenek listája.
 +
** ez fog először lefutni, a beküldött program előtt
 +
** például: <tt>["gcc", "-o", "myprogram", "myprogram.c"]</tt>
 +
** vagy ha több lépést szeretnénk: <tt>[["cmake", "."], ["make"]]</tt>
 +
** ha ez nincsen megadva, vagy üres lista van megadva, akkor nincsen fordítási lépés.
 +
** Ha bármelyik fordítási lépés '''nem-nulla hibakód'''ot ad, akkor a tesztek le sem futnak.
 +
* <tt>"command"</tt> ez fog lefutni tesztenként
 +
** lehet egy string vagy string-ek listája
 +
*** ha egy string akkor az a futtatható állomány fog lefutni, a tesztektől függő parancssori argumentumokkal
 +
*** ha string-ek listája, akkor [https://stackoverflow.com/a/47940538 docker exec] formátumban értendő, plusz esetleges parancssori argumentumok
 +
** például: <tt>"./myprogram"</tt>, ha előzőleg lefordítottuk
 +
** vagy <tt>["python3", "fahrenheit.py"]</tt>
 +
** vagy <tt>["wolfram", "-script", "calculate.m"]</tt>
 +
* Ezek a parancsok mind a beküldő felhasználójának home-mappájában (<tt>/home/dummy</tt>) fognak lefutni és a <tt>dummy</tt> felhasználó nevében (és jogosultságaival).
 +
* Ezen kívül az [[HazifeladatEllenorzoTeacher#manifest.json|általános]] description, deadline, ... mezők is lehetnek.
 +
 
 +
Egy teszt esethez egy '''<tt>i</tt>''' betűvel kezdődő nevű json fájlt kell berakni a feladat mappájába. Például <tt>ioverscrupulous.json</tt>:
 +
 
 +
{
 +
    "argv":  ["1.text", "overscrupulous"],
 +
    "file": ["1.text"],
 +
    "returncode": 0,
 +
    "stdout": "314\n240\1729"
 +
}
 +
Ennek kulcsai:
 +
* bemenet
 +
** argv
 +
*** például: <tt>"argv": ["a", "-h", "file.txt"]</tt>
 +
*** ezek a futtatandó parancs mögé append-álódnak
 +
** bemeneti fájlok, amiket olvashat a beküldő program a futása során
 +
*** egy string, vagy string-ek listája.
 +
*** ezeknek a fájloknak a nevei a feladat mappájától relatívak
 +
*** bemásolódnak az adott teszt előtt a beküldött program mellé
 +
*** példa: <tt>"file": "input.txt"</tt>
 +
** stdin, mit kapjon a standard bemeneten, egy string
 +
*** például: <tt>"stdin": "a\nb\n10\n"</tt>
 +
* kimenet
 +
** stdout, egy string, hogy mit várunk az stdout-ra.
 +
*** ha nincsen megadva, akkor mindegy, hogy mit írt ki az stdout-ra.
 +
** stderr, egy string, hogy mit várunk az stderr-ra.
 +
*** ha nincsen megadva, akkor mindegy, hogy mit írt ki az stderr-re.
 +
** returncode: milyen return code-al kell hogy megálljon a program
 +
*** ha nincsen megadva, akkor mindegy, hogy milyen hibakóddal lépett ki.
 +
 
 +
Opcionálisan megadhatunk egy (python3-ban írt) <tt>test.py</tt> nevű ellenőrző script-et, ami mindezen információk birtokában eldöntheti, hogy elfogadja-e a megoldást.
 +
<python>
 +
def _eval(_input, stdout, stderr, returncode):
 +
</python>
 +
Ennek True/False-t kell visszaadnia. Bemenete:
 +
* <tt>_input</tt>, lényegében a teszt json fájl, python dictionary-ként.
 +
* stdout: a program által írt kimenet, string
 +
* stderr: a program által írt hiba-kimenet, string
 +
* returncode: a program által visszaadott hibakód, int
 +
 
 +
=== python3type ===
 +
Akkor használjunk ilyen faladattípust, ha
 +
* python3 kódot szeretnénk kérni a megoldásban
 +
* például egy függvény vagy osztály megírása a feladat
 +
* nem számít hogy mit ír ki a beküldő az stdout és stderr-re, hanem valamilyen megadott kódnak kell a megfelelő visszatérési értéket adnia.
 +
 
 +
A <tt>manifest.json</tt> fájl tartalma
 +
* <tt>"type": "python3type"</tt>
 +
* <tt>"compile"</tt> opcionális, lásd a [[#program|program]] feladattípusnál
 +
* <tt>"code"</tt> mely kód fusson le, aminek a kimenetét akarjuk ellenőrizni
 +
** ha nincs megadva, akkor a következő:
 +
<python>
 +
def _code(_input):
 +
    return EXERCISE(*_input)
 +
</python>
 +
ahol <tt>EXERCISE</tt> helyett a feladat neve szerepel.
 +
 
 +
Ahogy eddig is, a feladat mappájába egy <tt>test.py</tt> fájlba rakhatunk saját ellenőrző kódot.
 +
Sőt itt olyan osztályokat is definiálhatunk, melyek nincsenek beépítve, de a megoldáshoz elengedhetetlenek.
 +
A kiértékelő kód alapértelmezésben:
 +
<python>
 +
def _eval(_input, _output, _expected_output, _exception, _expected_exception):
 +
    return type(_output) == type(_expected_output) and \
 +
          _output == _expected_output and \
 +
          type(_exception) == type(_expected_exception)
 +
</python>
 +
 
 +
Az <tt>_eval</tt> függvény paraméterei:
 +
* '''_input''' a bemeneti pickle fájlból betöltött objektum
 +
* '''_output''' a <tt>_code</tt> függvény által return-ölt objektum
 +
** vagy <tt>None</tt>, ha a függvény időközben Exception-t dobott
 +
* '''_expected_output''' a teszt által elvárt kimenet (az adott <tt>"o*.pkl"</tt> fájlból betöltött objektum)
 +
* '''_exception''' a függvény futása közben dobott kivétel
 +
** vagy <tt>None</tt> ha nem volt Exception
 +
* '''_expected_exception''' a teszt által elvárt kivétel, ha a teszt lényege az, hogy kivétel dobódjon
 +
** Ezt egy '''<tt>e</tt>''' betűvel kezdődő pickle fájlban tudjuk megadni, aminek a nevének többi része megegyezik a hozzá tartozó bemenetével.
 +
** Ha nincs ilyen fájl, akkor ez <tt>None</tt> lesz.
 +
 
 +
Ha az <tt>_eval</tt> függvény Exception-t dob, akkor az a teszt hibás lesz, de a kivételnek nem íródik ki a traceback-je, csak maga a kivétel.
 +
Ez azért fontos, mert ez más kategóriába esik, mintha a beküldő kódja dob kivételt, ami lehet elvárt is. Meg abból a szempontból is más ez a kivétel, hogy olyan kódban történt, ami ''priviliged''-ként fut, ezért nem érdemes nagyon hangoztatni. Elvileg a hibaüzenet tartalmazhatja is a kívánt megoldást.
 +
 
 +
==== függvény ====
 +
Ezt a feladattípust használhatjuk egy függvény megírására. Tegyük fel, hogy a feladat neve <tt>abc</tt> és egy pont ilyen nevű függvényt akarunk megíratni, aminem két paramétere van és a kettő összege a kimenete.
 +
<python>
 +
def abc(a, b):
 +
    return a + b
 +
    # ez már a megoldás
 +
</python>
 +
Ekkor a bemeneti és kimeneti pickle fájlokba valami ilyesmit kell tenni:
 +
* 0-adik teszt
 +
** <tt>i0.pkl</tt>: <tt>(0, 0)</tt>
 +
** <tt>o0.pkl</tt>: <tt>0</tt>
 +
* 1. teszt
 +
** <tt>i1.pkl</tt>: <tt>('a', 'b')</tt>
 +
** <tt>o1.pkl</tt>: <tt>'ab'</tt>
 +
 
 +
==== osztály ====
 +
Ezzel a feladattípussal feladhatjuk egy osztály megírását is.
 +
 
 +
* Legyen például a megírandó osztály neve <tt>Class</tt>, nem kell, hogy a feladat nevével megegyezzen.
 +
* Amit megkövetelünk ettől az osztálytól:
 +
** konstruktora kapjon egy paramétert (a self-en kívül)
 +
** tárolja el a kapott paramétert egy <tt>x</tt> nevű adattagban.
 +
** Ha nem egész számot kapott, emeljen <tt>ValueError</tt> kivételt
 +
 
 +
Ehhez a következő legyen a <tt>_code</tt> függvény (<tt>"code"</tt> mező a <tt>manifest.json</tt>-ban):
 +
<python>
 +
def _code(_input):
 +
    return Class(_input)
 +
</python>
 +
A teszt pickle-ök pedig az alábbiak:
 +
* 0-adik teszt
 +
** <tt>i0.pkl</tt>: <tt>3</tt>
 +
** <tt>o0.pkl</tt>: <tt>{'x': 3}</tt>
 +
* 1. teszt
 +
** <tt>i1.pkl</tt>: <tt>'a'</tt>
 +
** <tt>o1.pkl</tt>: <tt>None</tt>
 +
** <tt>e1.pkl</tt>: <tt>ValueError()</tt>
 +
És ami a legfontosabb, a <tt>test.py</tt>:
 +
<python>
 +
class Class:
 +
    pass
 +
 
 +
def _eval(_input, _output, _expected_output, _exception, _expected_exception):
 +
    return type(_expected_exception) == type(_exception) and type(_output) == Class and _output.__dict__ == _expected_output
 +
</python>
 +
Ez a következő képen fog működni.
 +
* Ha beküldő nem definiált <tt>Class</tt> osztályt egy egyparaméteres konstruktorral, akkor a <tt>_code</tt> függvény hibát fog dobni, és nem <tt>ValueError</tt> típusút. Ez az <tt>_eval</tt> függvény ellenőrzésén fenn fog akadni.
 +
* Ha a beküldő definiált egy <tt>Class</tt> egyváltozós ''függvényt'', de az nem egy <tt>Class</tt> típusú példányt ad vissza (nem is tudna), akkor a <tt>_eval</tt> függvény ellenőrzésén fennakad.
 +
* Ha a beküldő definiált egy <tt>Class</tt> osztályt a megfelelő konstruktorral, akkor a <tt>_code</tt> függvény egy példányt ad vissza, ami beleíródik a megfelelő output pickle fájlba (ha más kivétel nem volt).
 +
** Ezután az <tt>_eval</tt> függvény akkor tudja megkapni ezt az objektumot, ha a <tt>test.py</tt>-ban definiálva van egy <tt>Class</tt> prototípus.
 +
** Figyelem, mindegy hogy milyen (nem-statikus) tagváltozók vagy metódusok vannak az osztályban, mert a beküldő által létrehozott objektum lesz benne, nem az általunk megírt
 +
** Sőt a <tt>test.py</tt> fájlban nincs is példányosítás!
 +
** a pickle-nek elég ha létezik az a típus, mindegy hogy milyen metódusokkal, mert a példány adattagjait visszaolvasáskor nem a konstruktorral tölti fel, hanem máshogyan.
 +
* Így az <tt>eval</tt> függvény már le tuja ellenőrizni, hogy a kívánt típusú-e az objektum és hogy a kívánt adattagokat tartalmazza-e.
 +
 
 +
Figyelem, a <tt>test.py</tt>-ba ne oldjuk meg a feladatot, viszont érdemes ide egy '''konstruktort és egy <tt>__repr__</tt> metódust''' tenni.
 +
Technikailag a tesztek és a beküldések működni fognak akkor is, ha csak egy <tt>pass</tt> van az osztályban,
 +
mert ha a beküldő megírta rendesen az osztályt és a <tt>_code</tt>-ban az példányosult, akkor az már elég.
 +
 
 +
Figyelem, az <tt>_eval</tt> függvénybe ne tegyünk tagfüggvény hívást, se lépdányosítást! Ha ezt megtesszük, akkor annak az lesz a következménye,
 +
hogy a <tt>test.py</tt>-ban megírt kód fog lefutni, nem pedig a beküldő kódja!
 +
 
 +
Viszont a <tt>_code</tt> függvényben a beküldő kódja fog futni, viszont ide se tegyük be a megoldást, mert ezt kvázi láthatja a beküldő (stack-trace-el).
 +
=== text ===
 +
Ez a feladattípus egy szövegfájlt vár beküldésnek, amit aztán tetszésünk szerint értelmezhetünk.
 +
* A [[#manifest.json|manifest.json]] fájlba az általános mezőkön kívül nem lehet mást megadni.
 +
* A feladat mellé rakhatunk egy (python3-ban írt) <tt>test.py</tt> fájlt is, ami leellenőrzi a feladatot.
 +
* A feladat mellé rakhatunk egy <tt>solution.txt</tt> fájlt is, ami a mintamegoldást tartalmazza.
 +
* A feladat kiértékelése úgy történik, hogy a felhasználó által feltöltött fájl tartalmát összeveti a minta megoldással (ha van).
 +
* Ezt az <tt>_eval</tt> függvény végzi, mit a <tt>test.py</tt>-ban felüldefiniálhatunk, az alapértelmezett kiértékelő:
 +
<python>
 +
def _eval(_reference_text, _submitted_text):
 +
    return 1 if _reference_text.strip() == _submitted_text.strip() else 0
 +
</python>
 +
Ennek a függvénynek egy nem-negatív egész számot kell visszaadnia, az lesz a kapott pontszám.
 +
==== felelet választós ====
 +
Ezzel a feladattípussal lehet feleletválasztós kérdést is feltenni.
 +
Például:
 +
# Melyik a helyes?
 +
#* A: 1=0
 +
#* B: 1 != 0
 +
#* C: 1 < 0
 +
# Válaszd ki a legkisebb területűt:
 +
#* A: egység sugarú kör
 +
#* B: egy oldalhosszú négyzet
 +
#* C: Az ''x''<sup>2</sup> grafikonja és az y=0, x=0 és  x=1 egyenesek által közrefogott síkidom.
 +
 
 +
* Ekkor a válasz egy 2-soros szövegfájl, aminek mindegyik sora az ABC betűk valamelyike (pontosan az egyike)
 +
* A minta beküldés (<tt>solution.txt</tt>):
 +
B
 +
C
 +
*A kiértékelő függvény pedig a következő:
 +
<python>
 +
def _eval(_reference_text, _submitted_text):
 +
    submission = list(_submitted_text.strip().split())
 +
    solution = list(_reference_text.strip().split())
 +
    if len(submission) != len(solution):
 +
        print("Number of answers is {} but it should be {}!".format(len(submission), len(solution)))
 +
        return 0
 +
    return sum([submission[i] == solution[i] for i in range(len(submission))])
 +
</python>
 +
A kapott pontszám pedig 0-2-ig fog terjedni.
 +
 
 +
== build ==
 +
Ezzel a paranccsal lehet ''élesbe'' helyezni a rendszert.
 +
 
 +
cd ~ && docker build -f hazijavitorendszer/Dockerfile.release -t hazi_release hazijavitorendszer
 +
 
 +
* Ha ezt megtesszük, akkor a legközelebbi beküldés a build-elés pillanatában meglévő állapotokat fogja látni.
 +
* Amíg ezt nem tesszük meg, addig bármi lehet a <tt>hazijavitorendszer</tt> mappában, nem lesz hatással a hallgatók beküldéseire.
 +
* Figyelem, a <tt>hazijavitorendszer</tt> mappán kívüli fájlok/mappák módosítása ellen ez nem véd!
 +
 
 +
== definiálás ==
 +
Hogyan definiálhatunk új feladattípust? (Csak "expert"-eknek!)
  
 
= Mit NE csináljunk =
 
= Mit NE csináljunk =
Az egész rendszer működésének lényege, hogy egy levél megérkezésekor lefuttatja a <tt>run.sh</tt>-t. Ez a script meg kell hogy kapja a beérkezett levél tartalmát.
+
== chmod ==
Lehet egy mappa neve, amiben benne van a levél és a csatolmányai, vagy fájlok egy listája.
+
* A <tt>hazi</tt> felhasználó <tt>umask</tt>-ja 027, ezt ne változtassuk!
 +
* Minden mappa és fájl jogosultsága olyan, hogy <tt>other</tt> felhasználók ne lássák, ezt ne változtassuk!
 +
 
 +
== script-ek ==
 +
* A felhasználók táblázat és a feladatok mappáinak kivételével semmihez ne nyúljunk.
 +
** Csak "expert"-eknek!
 +
* Ezalatt értem a home-mappa tartalmát, illetve a HW mappában a script-eket.
 +
 
 +
= Logs =
 +
A rendszer folyamatosan megőrzi és eltárolja a beküldések legfontosabb adatait. De magát az eredeti beküldést csak a levelezőrendszer INBOX-ában tudjuk megnézni.
 +
 
 +
== log fájlok ==
 +
A <tt>logs</tt> mappában van minden log, amit a rendszer generált, ezek csak a beküldések kivonatai:
 +
* ki, mikor küldött be, melyik feladatot
 +
* Ha ''valid'' volt a beküldés (kurzus, határidő, beküldő mind rendben volt), akkor a pontszámát is
 +
* Ha egy beküldés nem volt ''valid'', de bizonyos gyenge követelményeket teljesített (pl. csak elkésett a beküldéssel), akkor a rendszer kiértékeli a feladatát, de nem ad rá pontot. Ezeket is láthatjuk a log-okban.
 +
 
 +
A log fájlok-ból ha valaki csak a pontokra kíváncsi, akkor a <tt><SUCCESS></tt> szóra kell <tt>grep</tt>-elni és az elért maximumot kikeresni.
 +
 
 +
Itt a második példában a beküldést kiértékelte a rendszer, de INVALID címkével (mondjuk úgyis 0 pontos lett volna):
 +
 
 +
[2020-02-17 13:42:47 UTC] <SUCCESS> submission from "borbely@math.bme.hu" exercise "greeting" returned 4
 +
[2020-02-17 13:42:48 UTC] INVALID submission from "borbely@math.bme.hu" exercise "koszones" returned 0
 +
 
 +
Ezekből a log-okból bizonyos [[#Pontok|segéd script]]-ekkel tudjuk kinyerni a pontokat.
 +
 
 +
== archivált ==
 +
Minden feladatról, minden beküldőről számon van tartva az ''adott pontszámot elért legutolsó'' beküldése.
 +
Ez úgy történik, hogy a rendszer (egészen pontosan az <tt>archive.sh</tt> script) mindig eltárolja a legutolsó beküldést, olyan fájlnévvel, ami tartalmazza a feladatot, a beküldőt és az elért pontszámot.
 +
 
 +
Ezt megnézhetjük az '''<tt>archive</tt>''' mappában.
 +
 
 +
    [archive]
 +
    ├───[feladat1]
 +
    │  ├───jozsi~1.py
 +
    │  ├───sanyi~2.py
 +
    │  ├───jozsi~2.py
 +
    │  ...
 +
    │
 +
    ├───[feladat2]
 +
    ├───[feladat3]
 +
    ...
 +
 
 +
Így mindig megkereshetjük a legutolsó beküldést, vagy a legjobb beküldést is. Vagy mondjuk csak a maximális pontot elért legutolsó beküldést.
 +
 
 +
= Segéd script-ek =
 +
== checksum ==
 +
Mivel az egész rendszer igen érzékeny minden benne lévő script-re, ezért van egy md5 checksum, ami teszteli, hogy nem írtunk-e bele véletlenül valamelyik fontos fájlba.
 +
 
 +
Ez a checksum kiszámolódik minden (interaktív) belépésnél, ezt látjuk itt:
 +
Using username "hazi".
 +
hazi@leibniz.math.bme.hu's password:
 +
 +
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
 +
permitted by applicable law.
 +
Last login: Thu Feb 20 14:30:24 2020 from 152.66.166.49
 +
'''f30a1e390a073b0f650ecfc6f0233ebf'''  -
 +
hazi@leibniz:~$
 +
 
 +
Build-elés előtt érdemes leellenőrizni, hogy még mindig ugyan az-e ez a checksum, mint ami a belépésnél volt.
 +
Ezt megtehetjük úgy is, hogy meghívjuk a <tt>checksums.sh</tt> bash script-et:
 +
 
 +
hazi@leibniz:~$ bash checksums.sh
 +
f30a1e390a073b0f650ecfc6f0233ebf  -
 +
hazi@leibniz:~$ bash checksums.sh -l
 +
 
 +
* Ennek a script-nek van egy <tt>-l</tt> opciója, ami nemcsak az összes fájlra vett MD5 hash-t írja ki, hanem fájlonként is.
 +
* A szükséges fájlok publikusan megtalálhatóak a [https://github.com/gaebor/hazi gaebor/hazi] github-on Ha valamit elrontunk, akkor innen vissza lehet nyerni a helyes fájlokat.
 +
** pullrequest-ek és issue-k nyugodtan jöhetnek.
 +
* Ez a hash érték nem érzékeny (többek között) az alábbiakra:
 +
** a feladatokra és a lefuttatandó tesztekre,  azokat mindenki elronthatja saját felelősségére (és kárára).
 +
** A log-okra és korábbi beküldésekre, pontok állására
 +
** a <tt>.tsv</tt> fájlok tartalmára, amiben a felhasználók vannak
 +
** A fentebb felsorol adatokról nincs is ''hivatalos'' mentés, ezeket minden feladatkitűzőnek magának kell megőriznie.
 +
 
 +
== Pontok ==
 +
bash checkpoints.sh "email" "feladat"
 +
Ez kiírja az adott (email címmel definiált) felhasználó adott feladatának pontszámát.
 +
 
 +
* Ha a felhasználó üres (<tt>""</tt>), akkor az adott feladat összes beküldőjének a pontját írja ki
 +
* Ha a feladat üres (<tt>""</tt>), akkor az adott ember összes beküldésének pontját írja ki
 +
* Ha mindegyik üres, akkor minden ember minden beküldésének max pontját írja ki.
 +
* Ez a script nem nézi az elkésett vagy érvénytelen beküldéseket, csak a minden beküldési feltételnek megfelelő feladatok pontszámát veszi figyelembe.
 +
* Lehet a pontokat az utolsó vagy a maximum szerint nézni
 +
** <tt>-m</tt> vagy <tt>--max</tt>, ez a default: legjobb érvényes beküldés
 +
** <tt>-l</tt> vagy <tt>--latest</tt>, a legutolsó, de még időben beküldött, eredményt nézi
 +
 
 +
== run.sh ==
 +
Ha a home mappából futtatjuk a '''run.sh''' script-et, akkor lehet szimulálni egy beküldést.
 +
* Ehhez meg kell adni a script-nek, hogy mely fájlokat és milyen levelet küldött egy (fiktív) beküldő.
 +
bash run.sh -h
 +
* Ez a script fut le akkor is, amikor valaki egy valódi levelet küld.
 +
* Ha megadjuk a <tt>--test</tt> kapcsolót az elején, akkor hasonló történik, csak
 +
** a válaszlevél nem elküldődik, hanem kiíródik a konzolra
 +
** Nincsen log-olás
 +
** Nincsen a megoldás kitörölve az ellenőrzés után (azért hogy újra lehessen tesztelni ugyanazzal a fájllal)
 +
** Nincsen archiválás
 +
** Mindig újra <tt>build</tt>-elődik a <tt>docker image</tt>, vagyis nem kell manuálisan megtennünk és nincs is hatással az ''éles'' beküldésekre
 +
* Ezzel érdemes kísérletezgetni, ha valaki új feladatokon dolgozik
 +
===kapcsolók===
 +
* teszt mód
 +
** <tt>--test</tt>
 +
** <tt>-t</tt>
 +
* help
 +
** <tt>--help</tt>
 +
** <tt>-h</tt>
 +
* a további argumentumok vagy egyetlen mappanév, vagy (egy vagy több) fájlnév
 +
** Ha mappanevet adunk meg, akkor a mappában lévő összes fájlt a beküldés részének tekinti.
 +
** Ha fájlt vagy fájlokat, akkor azon fájlokat tekinti a beküldés csatolmányainak
 +
===beküldés===
 +
* Figyelem, muszáj a beküldéshez legalább egy, kiterjesztés nélküli <tt>info</tt> nevű fájlt megadni, ami az (imitált) email adatait tartalmazza
 +
* Ha egy tényleges levél érkezik, akkor ezt a fájlt a levelezőrendszernek kell szolgáltatnia (ahogyan a csatolmányok letöltéséről is gondoskodik).
 +
* Az <tt>info</tt> fájl formátuma: legalább három soros utf8 kódolású szövegfájl
 +
*# sora a levél beküldője
 +
*#* Figyelem, a '''tényleges beküldő''' nem feltétlenül a levél '''From''' mezője, azt könnyű meghamisítani
 +
*# sora a levél tárgya
 +
*# sora a levél megérkezésének dátuma
 +
*#* Figyelem, '''nem a levél elküldésének dátuma'''!
 +
*#* Ennek '''kell időzóna''' információt is tartalmaznia, még ha UTC+0 is
 +
*#* a python <tt>dateutil.parser.parse</tt> függvényének fel kell tudnia olvasni
 +
*# további sorai a levél teste nyers szövegként
 +
 
 +
== pickle ==
 +
A python függvény típusú beküldéseknek a teszt fájljai bináris [https://docs.python.org/3/library/pickle.html pickle] fájlok.
 +
Ezeket kicsit körülményes szerkeszteni, ezért erre van egy segéd script.
 +
python3 makepickle.py -h
 +
Ez a parancssorban kapott argumentumokat pickle-özi.
 +
=== Kapcsolók ===
 +
*A kimenet (<tt>-o --output</tt>)
 +
** lehet stdout (ha üresen hagyjuk)
 +
** vagy egy fájlnév
 +
** vagy egy mappa, ekkor ebbe a mappába egy <tt>i[0-9]+.pkl</tt> nevű fájl lesz, olyan sorszámmal, ami még nincsen. Ez az új teszt bemenetekhez javasolt!
 +
* lista vagy egy elem (<tt>-l --list</tt>)
 +
** Ha ez a kapcsoló nincsen bekapcsolva, akkor egyszerűen az első parancssori argumentum lesz kimentve.
 +
** Ha be van kapcsolva, akkor az összes argumentum, mint lista lesz kimentve. A teszt bemenetekhez javasolt!

A lap jelenlegi, 2020. augusztus 25., 09:26-kori változata

Tartalomjegyzék

Tájékoztató

Ez az oldal a Házifeladat Ellenőrző rendszer használatát írja le, hogy hogyan lehet feladatokat feladni, felhasználókat és kurzusokat kezelni.

Belépés

A rendszer maga egy leibniz-es felhasználón keresztül érhető el:

  hazi@leibniz.math.bme.hu

Ide be lehet ssh-zni, scp-zni vagy a webmail-jébe belépni.

Honlap

Mivel a hazi egy sima felhasználó a leibniz-en, van honlapja is:

  http://math.bme.hu/~hazi

Ide lehet közérdekű (publikus) infókat kitenni, de lehet jelszóval védett al-oldalakat is csinálni, mondjuk azok biztonsága elég enyhe.

Érdekesség, hogy vannak terhelési grafikonok is.

Mappaszerkezet

Ennek a felhasználónak a home mappájában a következőket találjuk:

   [~]
   ├───[hazijavitorendszer]
   │   ├───[HW]
   │   │   ├───[feladat]
   │   │   .
   │   │   . (többi feladat)
   │   │   .
   │   │   ├───main
   │   │   ├───validate
   │   │   ├───userinfo.tsv
   │   │   ├───auxiliary.py
   │   │   ├───off
   │   │   ├───feladattipus1
   │   │   ├───feladattipus2
   │   │   ├───...
   │   │   └───getsenderinfo
   │   ├───Dockerfile
   │   └───Dockerfile.test
   ├───[solution]
   ├───[logs]
   ├───[archive]
   ├───digest_logs.sh
   ├───checkpoints.sh
   ├───...
   ├───archive.sh
   └───run.sh

Mit csináljunk

Hallgatók kezelése

Ezt lényegében egy hazijavitorendszer/HW/*.tsv (tab-separated-values) fájl szerkesztésével tehetjük meg.

Formátuma:

email	name	course
borbely@math.bme.hu	Gábor Borbély	info2,lecturer
  • Több ilyen fájl is lehet, ekkor a rendszer lényegében uniózza ezen fájlokat.
  • Minden sora egy felhasználó
  • A felhasználók az email-címükkel vannak azonosítva, a nevük csak tájékoztató jellegű.
  • Egy felhasználóhoz megadhatunk course-t, ami az általa látogatott kurzusok listája: egy vesszővel elválasztott lista.
  • Egy kurzus neve csak latin alfanumerikus karakterekből állhat (szóköz, vessző, ékezetes karakter nem lehet benne) vagy alulvonásból (azaz regex \w+).
    • A példában meg van adva egy lecturer csoport is a tanároknak.
  • Ha több kurzusra is jár egy hallgató, akkor a kurzust egy vesszővel vagy szóközzel elválasztott listával adjuk meg.
  • Ha egy hallgató többször szerepel a listában, akkor csak a legelső előfordulását vesszük figyelembe.
  • Régi hallgatókat nem érdemes kitörölni, ha a kurzusa évszámmal is meg van jelölve.
    • például a kurzus info2_2019 nem fog összeakadni az info2_2020 kurzussal
    • de ha valaki másodszorra hallgatja az info2-t, akkor lehet két kurzusa: info2_2019,info2_2020

Ha a felhasználók .tsv fájlját változtatjuk, akkor a módosítások csak akkor jutnak érvényre, ha újra build-eljük a docker image-et

Feladatok felvétele

Egy feladatot a hazijavitorendszer/HW/ mappában lévő mappa definiál.

  • a mappa neve a feladat neve
  • almappákat nem vesz figyelembe a rendszer, ezen belül nem lehet más almappa
  • kell legyen egy manifest.json fájl a feladat mappájában
  • kellenek tesztek a mappában
    • minden teszt neve i betűvel kell kezdődjön
    • a tesztek formátuma feladattípustól függ. lásd lentebb

Például a fahrenheit nevű feladat felvételéhez hozzuk létre az alábbiakat:

HW
└───fahrenheit
    ├───manifest.json
    ├───i1.json
    ├───i2.json
    └───i3.json

Egy feladathoz 123-nál több teszt esetet nem adhatunk meg!

Ha egy új feladatot felveszünk vagy régit módosítunk, vagy kitörlünk, akkor a módosítások csak akkor jutnak érvényre, ha újra build-eljük a docker image-et

manifest.json

A feladatot egy json dictionary írja le, az alábbi kulcsokkal:

  • "type" a feladat típusa, kell legyen egy, a típussal egyező nevű, futtatható fájl a HW mappában
  • "description" HTML source, json escaped, opcionális
  • "course" mely csoportok küldhetnek be (lecturer-t mindig érdemes belevenni)
    • ez lehet egy sztring, amiben vesszővel elválasztva vannak a kurzusok
    • vagy lehet egy json lista:
"course": ["info2", "lecturer"]

ha nem adunk meg kurzust, akkor senkitől nem fogad el beküldéseket

  • "visible" true vagy false
    • ha false akkor ez a feladat nem fogad el beküldéseket
  • "deadline" egy sztring, ami a határidőt írja le
    • például "2020-02-14 01:00:00 UTC+1"
    • Muszáj időzónát megadni, ha UTC időzónában adjuk meg, akkor is tegyünk egy +0-t az időpont mögé.
    • a python dateutils.parser.parse függvénye számára értelmezhető formátumban kell legyen
    • Ha nincsen egyáltalán "deadline" kulcs a szótárban, akkor bármikor be lehet küldeni a feladatot.
  • "disclaimer"
    • Ezzel megkövetelhetünk egy adott formátumú levéltörzset a beküldőtől.
    • Használható arra, hogy muszáj legyen beírni a hallgatónak azt, hogy ő készítette a feladatot és nem másolt.
    • Az értéke egy sztring kell legyen, amiben az alábbi behelyettesítéseket is megkövetelhetjük:
      • {name}
      • {email}
      • {course}
    • Például:
"disclaimer": "Én, {name}, felelősségem teljes tudatában kijelentem, hogy a mellékelt kód az én szellemi termékem, azt mással meg nem osztottam."
  • A disclaimer többnyelvű is lehet, ha egy json listában több ilyen sztringet is megadunk.
    • Ekkor az számít helyes beküldésnek, ha a levél törzse a megadott disclaimer-ek legalább egyikével megegyezik.
    • De egyszerűen megadhatunk üres disclaimer-t is, aminek a következménye, hogy csak üres levelet fogad el feladat.
    • Ha nincsen disclaimer kulcs a szótárban, akkor a levél törzse irreleváns.
  • "response"
    • Ezzel állíthatjuk, hogy a beküldő mit kapjon meg válaszként.
    • az értéke az alábbiak egyike (string-ként) vagy ezek listája:
      • "description" az eredmény mellé megkapja a feladat kiírását is. A feladat leírását egy speciális emaillel is meg lehet szerezni, szóval a beküldés után annyira nincsen szükség magára a feladatra, de érdemes ezt is beletenni a válaszemail-be.
      • "score" az elért pontszám, ha lehet, akkor azt is odaírja hogy mennyiből, de az egy feladatra kapható maximális pontszám nem jól definiált.
      • "tests" a kiértékelő script kimenete, ha ezt megadjuk és a kiértékelést végző kód kiírja hogy melyik teszt sikerült, akkor kvázi az elért pontszámot is elárultuk.
    • Ha nem adunk meg "response" mezőt a manifest-be, akkor a válasz mindent információt tartalmazni fog.

Feladattípusok

Egy feladat típusa határozza meg, hogy milyen programnyelvet várunk el a beküldőtől. Akkor tekinthető valami egy értelmes feladattípusnak, ha van egy olyan nevű futtatható fájl a HW mappában. Például ha programozási feladatból python3 programokat akarunk feladni, akkor kell legyen egy python3_program nevű futtatható állomány. A továbbiakban ez ellenőrzi le azt a feladatot, aminek a "type" mezőjében a "python3_program"-ot adtuk meg.

Lentebb részletezzünk, hogy milyen feladattípusok vannak és hogy melyik milyen sajátosságokkal rendelkezik. De definiálhatunk saját feladattípust is.

program

Általában bármilyen parancssorból hívható programot tesztelhetünk ezzel. Akkor is ha interpretált vagy ha fordított nyelven van írva. Persze ehhez installálva kell legyen a szükséges fordító és/vagy interpreter a Docker image-ben.

Akkor használjunk ilyen faladattípust, ha

  • azt akarjuk, hogy a beküldés egy interpreter által futtatott vagy fordító által fordított fájl legyen, ami parancssorból működik

Ahhoz hogy egy ilyen programot teszteljünk, az alábbiakat kell megadnunk a manifest.json fájlban:

  • "type": "program"
  • "compile" ennek az értéke egy parancs docker exec formátumban, vagy ilyenek listája.
    • ez fog először lefutni, a beküldött program előtt
    • például: ["gcc", "-o", "myprogram", "myprogram.c"]
    • vagy ha több lépést szeretnénk: [["cmake", "."], ["make"]]
    • ha ez nincsen megadva, vagy üres lista van megadva, akkor nincsen fordítási lépés.
    • Ha bármelyik fordítási lépés nem-nulla hibakódot ad, akkor a tesztek le sem futnak.
  • "command" ez fog lefutni tesztenként
    • lehet egy string vagy string-ek listája
      • ha egy string akkor az a futtatható állomány fog lefutni, a tesztektől függő parancssori argumentumokkal
      • ha string-ek listája, akkor docker exec formátumban értendő, plusz esetleges parancssori argumentumok
    • például: "./myprogram", ha előzőleg lefordítottuk
    • vagy ["python3", "fahrenheit.py"]
    • vagy ["wolfram", "-script", "calculate.m"]
  • Ezek a parancsok mind a beküldő felhasználójának home-mappájában (/home/dummy) fognak lefutni és a dummy felhasználó nevében (és jogosultságaival).
  • Ezen kívül az általános description, deadline, ... mezők is lehetnek.

Egy teszt esethez egy i betűvel kezdődő nevű json fájlt kell berakni a feladat mappájába. Például ioverscrupulous.json:

{
   "argv":  ["1.text", "overscrupulous"],
   "file": ["1.text"],
   "returncode": 0,
   "stdout": "314\n240\1729"
}

Ennek kulcsai:

  • bemenet
    • argv
      • például: "argv": ["a", "-h", "file.txt"]
      • ezek a futtatandó parancs mögé append-álódnak
    • bemeneti fájlok, amiket olvashat a beküldő program a futása során
      • egy string, vagy string-ek listája.
      • ezeknek a fájloknak a nevei a feladat mappájától relatívak
      • bemásolódnak az adott teszt előtt a beküldött program mellé
      • példa: "file": "input.txt"
    • stdin, mit kapjon a standard bemeneten, egy string
      • például: "stdin": "a\nb\n10\n"
  • kimenet
    • stdout, egy string, hogy mit várunk az stdout-ra.
      • ha nincsen megadva, akkor mindegy, hogy mit írt ki az stdout-ra.
    • stderr, egy string, hogy mit várunk az stderr-ra.
      • ha nincsen megadva, akkor mindegy, hogy mit írt ki az stderr-re.
    • returncode: milyen return code-al kell hogy megálljon a program
      • ha nincsen megadva, akkor mindegy, hogy milyen hibakóddal lépett ki.

Opcionálisan megadhatunk egy (python3-ban írt) test.py nevű ellenőrző script-et, ami mindezen információk birtokában eldöntheti, hogy elfogadja-e a megoldást.

 def _eval(_input, stdout, stderr, returncode):

Ennek True/False-t kell visszaadnia. Bemenete:

  • _input, lényegében a teszt json fájl, python dictionary-ként.
  • stdout: a program által írt kimenet, string
  • stderr: a program által írt hiba-kimenet, string
  • returncode: a program által visszaadott hibakód, int

python3type

Akkor használjunk ilyen faladattípust, ha

  • python3 kódot szeretnénk kérni a megoldásban
  • például egy függvény vagy osztály megírása a feladat
  • nem számít hogy mit ír ki a beküldő az stdout és stderr-re, hanem valamilyen megadott kódnak kell a megfelelő visszatérési értéket adnia.

A manifest.json fájl tartalma

  • "type": "python3type"
  • "compile" opcionális, lásd a program feladattípusnál
  • "code" mely kód fusson le, aminek a kimenetét akarjuk ellenőrizni
    • ha nincs megadva, akkor a következő:
def _code(_input):
    return EXERCISE(*_input)

ahol EXERCISE helyett a feladat neve szerepel.

Ahogy eddig is, a feladat mappájába egy test.py fájlba rakhatunk saját ellenőrző kódot. Sőt itt olyan osztályokat is definiálhatunk, melyek nincsenek beépítve, de a megoldáshoz elengedhetetlenek. A kiértékelő kód alapértelmezésben:

def _eval(_input, _output, _expected_output, _exception, _expected_exception):
    return type(_output) == type(_expected_output) and \
           _output == _expected_output and \
           type(_exception) == type(_expected_exception)

Az _eval függvény paraméterei:

  • _input a bemeneti pickle fájlból betöltött objektum
  • _output a _code függvény által return-ölt objektum
    • vagy None, ha a függvény időközben Exception-t dobott
  • _expected_output a teszt által elvárt kimenet (az adott "o*.pkl" fájlból betöltött objektum)
  • _exception a függvény futása közben dobott kivétel
    • vagy None ha nem volt Exception
  • _expected_exception a teszt által elvárt kivétel, ha a teszt lényege az, hogy kivétel dobódjon
    • Ezt egy e betűvel kezdődő pickle fájlban tudjuk megadni, aminek a nevének többi része megegyezik a hozzá tartozó bemenetével.
    • Ha nincs ilyen fájl, akkor ez None lesz.

Ha az _eval függvény Exception-t dob, akkor az a teszt hibás lesz, de a kivételnek nem íródik ki a traceback-je, csak maga a kivétel. Ez azért fontos, mert ez más kategóriába esik, mintha a beküldő kódja dob kivételt, ami lehet elvárt is. Meg abból a szempontból is más ez a kivétel, hogy olyan kódban történt, ami priviliged-ként fut, ezért nem érdemes nagyon hangoztatni. Elvileg a hibaüzenet tartalmazhatja is a kívánt megoldást.

függvény

Ezt a feladattípust használhatjuk egy függvény megírására. Tegyük fel, hogy a feladat neve abc és egy pont ilyen nevű függvényt akarunk megíratni, aminem két paramétere van és a kettő összege a kimenete.

def abc(a, b):
    return a + b
    # ez már a megoldás

Ekkor a bemeneti és kimeneti pickle fájlokba valami ilyesmit kell tenni:

  • 0-adik teszt
    • i0.pkl: (0, 0)
    • o0.pkl: 0
  • 1. teszt
    • i1.pkl: ('a', 'b')
    • o1.pkl: 'ab'

osztály

Ezzel a feladattípussal feladhatjuk egy osztály megírását is.

  • Legyen például a megírandó osztály neve Class, nem kell, hogy a feladat nevével megegyezzen.
  • Amit megkövetelünk ettől az osztálytól:
    • konstruktora kapjon egy paramétert (a self-en kívül)
    • tárolja el a kapott paramétert egy x nevű adattagban.
    • Ha nem egész számot kapott, emeljen ValueError kivételt

Ehhez a következő legyen a _code függvény ("code" mező a manifest.json-ban):

def _code(_input):
    return Class(_input)

A teszt pickle-ök pedig az alábbiak:

  • 0-adik teszt
    • i0.pkl: 3
    • o0.pkl: {'x': 3}
  • 1. teszt
    • i1.pkl: 'a'
    • o1.pkl: None
    • e1.pkl: ValueError()

És ami a legfontosabb, a test.py:

class Class:
    pass
 
def _eval(_input, _output, _expected_output, _exception, _expected_exception):
    return type(_expected_exception) == type(_exception) and type(_output) == Class and _output.__dict__ == _expected_output

Ez a következő képen fog működni.

  • Ha beküldő nem definiált Class osztályt egy egyparaméteres konstruktorral, akkor a _code függvény hibát fog dobni, és nem ValueError típusút. Ez az _eval függvény ellenőrzésén fenn fog akadni.
  • Ha a beküldő definiált egy Class egyváltozós függvényt, de az nem egy Class típusú példányt ad vissza (nem is tudna), akkor a _eval függvény ellenőrzésén fennakad.
  • Ha a beküldő definiált egy Class osztályt a megfelelő konstruktorral, akkor a _code függvény egy példányt ad vissza, ami beleíródik a megfelelő output pickle fájlba (ha más kivétel nem volt).
    • Ezután az _eval függvény akkor tudja megkapni ezt az objektumot, ha a test.py-ban definiálva van egy Class prototípus.
    • Figyelem, mindegy hogy milyen (nem-statikus) tagváltozók vagy metódusok vannak az osztályban, mert a beküldő által létrehozott objektum lesz benne, nem az általunk megírt
    • Sőt a test.py fájlban nincs is példányosítás!
    • a pickle-nek elég ha létezik az a típus, mindegy hogy milyen metódusokkal, mert a példány adattagjait visszaolvasáskor nem a konstruktorral tölti fel, hanem máshogyan.
  • Így az eval függvény már le tuja ellenőrizni, hogy a kívánt típusú-e az objektum és hogy a kívánt adattagokat tartalmazza-e.

Figyelem, a test.py-ba ne oldjuk meg a feladatot, viszont érdemes ide egy konstruktort és egy __repr__ metódust tenni. Technikailag a tesztek és a beküldések működni fognak akkor is, ha csak egy pass van az osztályban, mert ha a beküldő megírta rendesen az osztályt és a _code-ban az példányosult, akkor az már elég.

Figyelem, az _eval függvénybe ne tegyünk tagfüggvény hívást, se lépdányosítást! Ha ezt megtesszük, akkor annak az lesz a következménye, hogy a test.py-ban megírt kód fog lefutni, nem pedig a beküldő kódja!

Viszont a _code függvényben a beküldő kódja fog futni, viszont ide se tegyük be a megoldást, mert ezt kvázi láthatja a beküldő (stack-trace-el).

text

Ez a feladattípus egy szövegfájlt vár beküldésnek, amit aztán tetszésünk szerint értelmezhetünk.

  • A manifest.json fájlba az általános mezőkön kívül nem lehet mást megadni.
  • A feladat mellé rakhatunk egy (python3-ban írt) test.py fájlt is, ami leellenőrzi a feladatot.
  • A feladat mellé rakhatunk egy solution.txt fájlt is, ami a mintamegoldást tartalmazza.
  • A feladat kiértékelése úgy történik, hogy a felhasználó által feltöltött fájl tartalmát összeveti a minta megoldással (ha van).
  • Ezt az _eval függvény végzi, mit a test.py-ban felüldefiniálhatunk, az alapértelmezett kiértékelő:
def _eval(_reference_text, _submitted_text):
    return 1 if _reference_text.strip() == _submitted_text.strip() else 0

Ennek a függvénynek egy nem-negatív egész számot kell visszaadnia, az lesz a kapott pontszám.

felelet választós

Ezzel a feladattípussal lehet feleletválasztós kérdést is feltenni. Például:

  1. Melyik a helyes?
    • A: 1=0
    • B: 1 != 0
    • C: 1 < 0
  2. Válaszd ki a legkisebb területűt:
    • A: egység sugarú kör
    • B: egy oldalhosszú négyzet
    • C: Az x2 grafikonja és az y=0, x=0 és x=1 egyenesek által közrefogott síkidom.
  • Ekkor a válasz egy 2-soros szövegfájl, aminek mindegyik sora az ABC betűk valamelyike (pontosan az egyike)
  • A minta beküldés (solution.txt):
B
C
  • A kiértékelő függvény pedig a következő:
def _eval(_reference_text, _submitted_text):
    submission = list(_submitted_text.strip().split())
    solution = list(_reference_text.strip().split())
    if len(submission) != len(solution):
        print("Number of answers is {} but it should be {}!".format(len(submission), len(solution)))
        return 0
    return sum([submission[i] == solution[i] for i in range(len(submission))])

A kapott pontszám pedig 0-2-ig fog terjedni.

build

Ezzel a paranccsal lehet élesbe helyezni a rendszert.

cd ~ && docker build -f hazijavitorendszer/Dockerfile.release -t hazi_release hazijavitorendszer
  • Ha ezt megtesszük, akkor a legközelebbi beküldés a build-elés pillanatában meglévő állapotokat fogja látni.
  • Amíg ezt nem tesszük meg, addig bármi lehet a hazijavitorendszer mappában, nem lesz hatással a hallgatók beküldéseire.
  • Figyelem, a hazijavitorendszer mappán kívüli fájlok/mappák módosítása ellen ez nem véd!

definiálás

Hogyan definiálhatunk új feladattípust? (Csak "expert"-eknek!)

Mit NE csináljunk

chmod

  • A hazi felhasználó umask-ja 027, ezt ne változtassuk!
  • Minden mappa és fájl jogosultsága olyan, hogy other felhasználók ne lássák, ezt ne változtassuk!

script-ek

  • A felhasználók táblázat és a feladatok mappáinak kivételével semmihez ne nyúljunk.
    • Csak "expert"-eknek!
  • Ezalatt értem a home-mappa tartalmát, illetve a HW mappában a script-eket.

Logs

A rendszer folyamatosan megőrzi és eltárolja a beküldések legfontosabb adatait. De magát az eredeti beküldést csak a levelezőrendszer INBOX-ában tudjuk megnézni.

log fájlok

A logs mappában van minden log, amit a rendszer generált, ezek csak a beküldések kivonatai:

  • ki, mikor küldött be, melyik feladatot
  • Ha valid volt a beküldés (kurzus, határidő, beküldő mind rendben volt), akkor a pontszámát is
  • Ha egy beküldés nem volt valid, de bizonyos gyenge követelményeket teljesített (pl. csak elkésett a beküldéssel), akkor a rendszer kiértékeli a feladatát, de nem ad rá pontot. Ezeket is láthatjuk a log-okban.

A log fájlok-ból ha valaki csak a pontokra kíváncsi, akkor a <SUCCESS> szóra kell grep-elni és az elért maximumot kikeresni.

Itt a második példában a beküldést kiértékelte a rendszer, de INVALID címkével (mondjuk úgyis 0 pontos lett volna):

[2020-02-17 13:42:47 UTC] <SUCCESS> submission from "borbely@math.bme.hu" exercise "greeting" returned 4
[2020-02-17 13:42:48 UTC] INVALID submission from "borbely@math.bme.hu" exercise "koszones" returned 0

Ezekből a log-okból bizonyos segéd script-ekkel tudjuk kinyerni a pontokat.

archivált

Minden feladatról, minden beküldőről számon van tartva az adott pontszámot elért legutolsó beküldése. Ez úgy történik, hogy a rendszer (egészen pontosan az archive.sh script) mindig eltárolja a legutolsó beküldést, olyan fájlnévvel, ami tartalmazza a feladatot, a beküldőt és az elért pontszámot.

Ezt megnézhetjük az archive mappában.

   [archive]
   ├───[feladat1]
   │   ├───jozsi~1.py
   │   ├───sanyi~2.py
   │   ├───jozsi~2.py
   │   ...
   │
   ├───[feladat2]
   ├───[feladat3]
   ...

Így mindig megkereshetjük a legutolsó beküldést, vagy a legjobb beküldést is. Vagy mondjuk csak a maximális pontot elért legutolsó beküldést.

Segéd script-ek

checksum

Mivel az egész rendszer igen érzékeny minden benne lévő script-re, ezért van egy md5 checksum, ami teszteli, hogy nem írtunk-e bele véletlenül valamelyik fontos fájlba.

Ez a checksum kiszámolódik minden (interaktív) belépésnél, ezt látjuk itt:

Using username "hazi".
hazi@leibniz.math.bme.hu's password:

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Feb 20 14:30:24 2020 from 152.66.166.49
f30a1e390a073b0f650ecfc6f0233ebf  -
hazi@leibniz:~$

Build-elés előtt érdemes leellenőrizni, hogy még mindig ugyan az-e ez a checksum, mint ami a belépésnél volt. Ezt megtehetjük úgy is, hogy meghívjuk a checksums.sh bash script-et:

hazi@leibniz:~$ bash checksums.sh
f30a1e390a073b0f650ecfc6f0233ebf  -
hazi@leibniz:~$ bash checksums.sh -l
  • Ennek a script-nek van egy -l opciója, ami nemcsak az összes fájlra vett MD5 hash-t írja ki, hanem fájlonként is.
  • A szükséges fájlok publikusan megtalálhatóak a gaebor/hazi github-on Ha valamit elrontunk, akkor innen vissza lehet nyerni a helyes fájlokat.
    • pullrequest-ek és issue-k nyugodtan jöhetnek.
  • Ez a hash érték nem érzékeny (többek között) az alábbiakra:
    • a feladatokra és a lefuttatandó tesztekre, azokat mindenki elronthatja saját felelősségére (és kárára).
    • A log-okra és korábbi beküldésekre, pontok állására
    • a .tsv fájlok tartalmára, amiben a felhasználók vannak
    • A fentebb felsorol adatokról nincs is hivatalos mentés, ezeket minden feladatkitűzőnek magának kell megőriznie.

Pontok

bash checkpoints.sh "email" "feladat"

Ez kiírja az adott (email címmel definiált) felhasználó adott feladatának pontszámát.

  • Ha a felhasználó üres (""), akkor az adott feladat összes beküldőjének a pontját írja ki
  • Ha a feladat üres (""), akkor az adott ember összes beküldésének pontját írja ki
  • Ha mindegyik üres, akkor minden ember minden beküldésének max pontját írja ki.
  • Ez a script nem nézi az elkésett vagy érvénytelen beküldéseket, csak a minden beküldési feltételnek megfelelő feladatok pontszámát veszi figyelembe.
  • Lehet a pontokat az utolsó vagy a maximum szerint nézni
    • -m vagy --max, ez a default: legjobb érvényes beküldés
    • -l vagy --latest, a legutolsó, de még időben beküldött, eredményt nézi

run.sh

Ha a home mappából futtatjuk a run.sh script-et, akkor lehet szimulálni egy beküldést.

  • Ehhez meg kell adni a script-nek, hogy mely fájlokat és milyen levelet küldött egy (fiktív) beküldő.
bash run.sh -h
  • Ez a script fut le akkor is, amikor valaki egy valódi levelet küld.
  • Ha megadjuk a --test kapcsolót az elején, akkor hasonló történik, csak
    • a válaszlevél nem elküldődik, hanem kiíródik a konzolra
    • Nincsen log-olás
    • Nincsen a megoldás kitörölve az ellenőrzés után (azért hogy újra lehessen tesztelni ugyanazzal a fájllal)
    • Nincsen archiválás
    • Mindig újra build-elődik a docker image, vagyis nem kell manuálisan megtennünk és nincs is hatással az éles beküldésekre
  • Ezzel érdemes kísérletezgetni, ha valaki új feladatokon dolgozik

kapcsolók

  • teszt mód
    • --test
    • -t
  • help
    • --help
    • -h
  • a további argumentumok vagy egyetlen mappanév, vagy (egy vagy több) fájlnév
    • Ha mappanevet adunk meg, akkor a mappában lévő összes fájlt a beküldés részének tekinti.
    • Ha fájlt vagy fájlokat, akkor azon fájlokat tekinti a beküldés csatolmányainak

beküldés

  • Figyelem, muszáj a beküldéshez legalább egy, kiterjesztés nélküli info nevű fájlt megadni, ami az (imitált) email adatait tartalmazza
  • Ha egy tényleges levél érkezik, akkor ezt a fájlt a levelezőrendszernek kell szolgáltatnia (ahogyan a csatolmányok letöltéséről is gondoskodik).
  • Az info fájl formátuma: legalább három soros utf8 kódolású szövegfájl
    1. sora a levél beküldője
      • Figyelem, a tényleges beküldő nem feltétlenül a levél From mezője, azt könnyű meghamisítani
    2. sora a levél tárgya
    3. sora a levél megérkezésének dátuma
      • Figyelem, nem a levél elküldésének dátuma!
      • Ennek kell időzóna információt is tartalmaznia, még ha UTC+0 is
      • a python dateutil.parser.parse függvényének fel kell tudnia olvasni
    4. további sorai a levél teste nyers szövegként

pickle

A python függvény típusú beküldéseknek a teszt fájljai bináris pickle fájlok. Ezeket kicsit körülményes szerkeszteni, ezért erre van egy segéd script.

python3 makepickle.py -h

Ez a parancssorban kapott argumentumokat pickle-özi.

Kapcsolók

  • A kimenet (-o --output)
    • lehet stdout (ha üresen hagyjuk)
    • vagy egy fájlnév
    • vagy egy mappa, ekkor ebbe a mappába egy i[0-9]+.pkl nevű fájl lesz, olyan sorszámmal, ami még nincsen. Ez az új teszt bemenetekhez javasolt!
  • lista vagy egy elem (-l --list)
    • Ha ez a kapcsoló nincsen bekapcsolva, akkor egyszerűen az első parancssori argumentum lesz kimentve.
    • Ha be van kapcsolva, akkor az összes argumentum, mint lista lesz kimentve. A teszt bemenetekhez javasolt!
Személyes eszközök