Kihagyás

GitLab CI

Szerkesztés alatt!

Az oldal további része szerkesztés alatt áll, a tartalma minden további értesítés nélkül többször, gyakran, jelentősen megváltozhat!

CI/CD: Continuous Integration / Continuous Delivery / Continuous Deployment

DevOps

A DevOps a Development és Operations szavak összevonásából keletkezett. Egy olyan fejlesztési filozófiát takar, ami a "hagyományosabb" fejlesztési modelleket (amik a szoftver tervezésétől a release-ig tartanak, esetleg karbantartással kiegészítve) egy működ(tet)ési résszel egészíti ki, és ezt (ennek tapasztalatait) visszacsatolja a fejlesztésbe. A DevOps jól illeszkedik az agilis fejlesztéshez abban az elvben, hogy a szoftver soha nincs készen, mindig lesz mit javítani/fejleszteni rajta.

x

A DevOps-nak része a Continuous Integration / Continuous Delivery / Continuous Deployment (CI/CD), aminek az alapja, hogy a szoftver mindig "release" állapotban van. Vagyis (a CI rész szerint) úgy kell fejleszteni, hogy a szoftver fő verzióján eszközölt bármely módosítás után is a szoftver használható, kiadható állapotban legyen, és (a CD rész szerint) a "release" (lehetőleg automatikusan) meg is történjen, és ha lehet, akkor az új release be is legyen üzemelve. A CI/CD tehát magában foglalja a folyamatok nagyfokú automatizálását is.

Ehhez az automatizáláshoz nyújt segítséget a GitLab CI mechanizmus.

A GitLab CI

A GitLab CI alapvetően úgy működik, hogy meghatározhatunk, előírhatunk feladatokat, és ha a GitLab a repository-ban változást észlel (volt egy push, voltak commit-ok), akkor végrehajtja ezeket a feladatokat. Ehhez külön erőforrást kell rendelnünk, egy gépet ("runner"), ami képes elvégezni ezeket a feladatokat, de ennek a beállításával, felkonfigurálásával most nem foglalkozunk -- feltesszük, hogy adott, és a projektünkből elérhető, használható.

A GitLab CI Docker alapokon működik. Ez egy virtualizációs technika (kicsit komolyabb, mint a venv, de nem olyan teljes, mint egy VirtualBox vagy VMware). A lényeg, hogy meg kell adnunk egy "image"-et, egy előre meghatározott környezetet, amiben majd a parancsaink, szkriptjeink futnak. A runner tulajdonképpen ezt az image-et indítja el és futtatja benne a parancsainkat (a repository adott verziójának gyökérkönyvtárából).

A GitLab CI/CD pipeline használata

A GitLab CI használatához a projektünk gyökerében létre kell hozni egy .gitlab-ci.yml nevű konfigurációs fájlt, a GitLab ez alapján fog dolgozni. Ebben elég sok mindent be lehet konfigurálni, akit érdekel, az részletes leírást talál a https://docs.gitlab.com/ci/yaml oldalon.

Példa .gitlab-ci.yml konfigurációs fájl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
stages:
  - setup
  - build
  - test
  - deploy

default:
  image: python:3.10.17

before_script:
  - pip3 install -r requirements.txt

after_script:
  - echo "Finishing the job"

unit:
  stage: test
  tags:
    - test
  script:
    - pytest --junitxml=./test_unit.xml test/test_geometry_unit.py
  allow_failure: false
  dependencies:
    - setup
  artifacts:
    when: always
    paths:
      - test_unit.xml
    expire_in: "30 days"
    reports:
      junit: ./test_unit.xml

integration:
  stage: test
  tags:
    - test
  script:
    - pytest --junitxml=./test_integ.xml test/test_geometry_integ.py
  allow_failure: false
  dependencies:
    - setup
    - unit
  artifacts:
    when: always
    paths:
      - test_integ.xml
    expire_in: "30 days"
    reports:
      junit: ./test_integ.xml

deploy:
  stage: deploy
  tags:
    - deploy
  only:
    - main
  script:
    - echo "Deploying the application..."
    - echo "Deployment complete!"
  allow_failure: false
  dependencies:
    - unit
    - integration

A konfiguráció főbb elemei:

Stages

A stages: pont határozza meg, hogy a teljes CI/CD folyamatnak milyen fázisai lesznek. Az egyes fázisok a megadott sorrendben egymás után jönnek, de egy fázisban több feladatot is elvégezhetünk. A fázison belül a feladatok (elvileg) párhuzamosítva futnak, hacsak nincs közöttük függőség megadva.

Stages

1
2
3
4
5
stages:
  - setup
  - build
  - test
  - deploy

Ez a kódrészlet négy fázist határoz meg, setup, build, test és deploy néven, amelyek ilyen sorrendben követik egymást. A fázisok nevei tetszőlegesek, de azért nem árt, ha utalnak arra, hogy mire valók.

Általános beállítások

A stages: beállítással egy szinten olyan dolgokat tudunk megadni, amelyek minden feladatra érvényesek lesznek. Egy adott feladaton belül természetestn ezek a beállítások felülbírálhatók.

Általános beállítások

1
2
3
4
5
6
7
8
default:
  image: python:3.10.17

before_script:
  - pip3 install -r requirements.txt

after_script:
  - echo "Finishing the job"

A default-on belül az image megadja a használni kívánt docker image-et környezetet. Ha a feladatban ezt nem írjuk felül, akkor ezt a default image-et fogja használni.

A before_script és after_script minden feladatban a saját szkript/parancsok előtt/után lefut. A feladatban ez is felülírható.

Konkrét feladatok

A konkrét feladatokat mindig úgy kell elképzelni, hogy elindul a megfelelő docker image, ezután checkout-oljuk a repository aktuális verzióját (amelyik verzióban a változás történt), majd ennek a gyökerében kezdünk el dolgozni.

Feladat (job): setup

1
2
3
4
5
6
7
8
setup:
  stage: setup
  tags:
    - setup
  script:
    - echo "Setting up the application..."
    - echo "Setup complete!"
  allow_failure: false

A setup itt a feladat neve, ami majd a setup fázisban (stage) fut le.

Ha a GitLab runner(ek) úgy van(nak) konfigurálva, hogy bizonyos runnerek csak bizonyos feladatokat képesek futtatni, akkor a tags beállítással tudjuk megadni, hogy ez a feladat mely runnereken futhat. Egy runner többféle feladat végrahajtására is képes lehet.

A script-ben lehet megadni a futtatandó parancsokat, ezeket a rendszer szépen sorban fogja végrehajtani.

Ha az allow_failure beállítással megengedjük a hibát, akkor a többi feladat és fázis hiba esetén is lefut; különben a teljes CI pipeline hibával leáll.

Feladat (job): egység teszt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
unit:
  stage: test
  tags:
    - test
  script:
    - python3 -m pytest --junitxml=./test_unit.xml test/test_geometry_unit.py
  allow_failure: false
  dependencies:
    - setup
  artifacts:
    when: always
    paths:
      - test_unit.xml
    expire_in: "30 days"
    reports:
      junit: ./test_unit.xml

A unit itt a feladat neve, ami majd a test fázisban (stage) fut le.

A script-ben most úgy indítjuk el a pytest modult, hogy az az eredményeket a test_unit.xml-be (is) beleírja, és csak a test/test_geometry_unit.py-ben található teszteket futtassa.

A dependencies megadja, hogy mely feladatoknak kell sikeresen lefutnia ahhoz, hogy ez a feladat értelmesen elindítható legyen.

Az atrifacts beállítások azt mondják meg, hogy a GitLab mely fájlokat őrízze meg (és meddig) ebből a feladatból. A when: always azt írja elő, hogy ha a feladat bukik (van hibás teszt), akkor is legyen eltárolva a riport. A paths megadja, hogy mely fájlokról van szó, az expire_in pedig, hogy milyen hosszan kell tárolni. A reports és az alatti junit rész speciális, a GitLab ezek után ezt a fájlt egységteszt-futtatás junit formátumú eredményeként tudja kezelni.

Feladat (job): integrációs teszt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
integration:
  stage: test
  tags:
    - test
  script:
    - pytest --junitxml=./test_integ.xml test/test_geometry_integ.py
  allow_failure: false
  dependencies:
    - setup
    - unit
  artifacts:
    when: always
    paths:
      - test_integ.xml
    expire_in: "30 days"
    reports:
      junit: ./test_integ.xml

Az integration itt a feladat neve, ami majd a test fázisban (stage) fut le.

A script-ben most úgy indítjuk el a pytest modult, hogy az az eredményeket a test_integ.xml-be (is) beleírja, és csak a test/test_geometry_integ.py-ben található teszteket futtassa.

A dependencies megadja, hogy mely feladatoknak kell sikeresen lefutnia ahhoz, hogy ez a feladat értelmesen elindítható legyen. Jelen esetben az integrációs teszteknek nyilván nincs értelme, ha már az egységtesztek sem futnak.

Kicsit furcsának tűnhet, hogy a reports megadásával ezt az integrációs tesztet is unit tesztként kezeljük, de nincs ellentmondás. A junit az egy elterjet (Java-s) unit teszt keretrendszer -- hasonló a pytest-hez (bár történelmileg lehet, hogy a "pytest hasonlít a junit-hoz" lenne hitelesebb állítás) -- és a formátum erről kapta a nevét. (Ahogy eredetileg a "trafipax" sem a sebességmérő eszközök vagy a "kuka" sem a szemétgyűjtők általános elnevezése, hanem a gyártóik neve volt.) Azt pedig már megbeszéltük, hogy az egységteszt keretrendszerekkel integrációs teszteket is lehet készíteni.

Feladat (job): deploy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
deploy:
  stage: deploy
  tags:
    - deploy
  only:
    - main
  script:
    - echo "Deploying the application..."
    - echo "Deployment complete!"
  allow_failure: false
  dependencies:
    - unit
    - integration

A deploy itt a feladat neve, ami majd a deploy fázisban (stage) fut le.

Az only-val jelezhetjük, hogy ezt a feladatot csak akkor kell futtatni, ha a main-ben van változás. A tesztek mindig, minden változásra lefutottak, de a szoftvernek csak a main verzióját tesszük elérhetővé; és a dependencies alapján azt is csak akkor, ha a tesztek sikeresen lefutottak. A script részben kellene kiadni azokat a parancsokat, amik feltöltik valahová a szoftver aktuális verzióját (ha olyan a szoftver, akkor a build fázisban belőle készített valamilyen artifact-et -- Python projektet feltételezve, ahol a forrás maga futtatható, így nincs szükség külön build lépésre, most a build feladat ki is maradt), de erre most nem adunk példát.

Példa

Adott a következő -- fejlesztés alatt álló, nem kész -- chess python package:

chess/__init__.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"""
Szoftverfesztelési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd

A chess package __init__.py fájlja.
"""

from . import constants
from . import board
from . import figures
chess/board.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""
Szoftverfesztelési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd

A chess package board modulja.
"""

from . import constants

from rich.console import Console

"""
Új tábla létrehozása, melyen a bábuk a kezdőállásnak megfelelően állnak.
"""
def new_default_table():
    return [
        [{'fig':constants.FROOK, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FROOK, 'col':constants.CDRK}],
        [{'fig':constants.FKNGT, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FKNGT, 'col':constants.CDRK}],
        [{'fig':constants.FBSHP, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FBSHP, 'col':constants.CDRK}],
        [{'fig':constants.FQUEN, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FQUEN, 'col':constants.CDRK}],
        [{'fig':constants.FKING, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FKING, 'col':constants.CDRK}],
        [{'fig':constants.FBSHP, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FBSHP, 'col':constants.CDRK}],
        [{'fig':constants.FKNGT, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FKNGT, 'col':constants.CDRK}],
        [{'fig':constants.FROOK, 'col':constants.CLGT},{'fig':constants.FPAWN, 'col':constants.CLGT},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':constants.FPAWN, 'col':constants.CDRK},{'fig':constants.FROOK, 'col':constants.CDRK}]
    ]

"""
Új, üres tábla létrehozása.
"""
def new_empty_table():
    return [
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}],
        [{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None},{'fig':None, 'col':None}]
    ]

"""
A kapott tábla és a rajta lévő állás kirajzolása a konzolra.
"""
def print_board(board, con=Console()):
    con.print("   A B C D E F G H  ", style="bold cyan on black")
    for r in range(7, -1, -1):
        con.print(f"{r+1} ", style="bold cyan on black", end='')
        for c in range(0, 8):
            black = (r + c) % 2
            samec = board[c][r]['col'] == (constants.CDRK if black else constants.CLGT)
            cc = 'black' if black else 'white'
            fc = 'white' if black else 'black'
            match board[c][r]['fig']:
                case constants.FPAWN:
                    fg = f"{constants.CSPAWN if samec else constants.CIPAWN} "
                case constants.FKNGT:
                    fg = f"{constants.CSKNGT if samec else constants.CIKNGT} "
                case constants.FBSHP:
                    fg = f"{constants.CSBSHP if samec else constants.CIBSHP} "
                case constants.FROOK:
                    fg = f"{constants.CSROOK if samec else constants.CIROOK} "
                case constants.FQUEN:
                    fg = f"{constants.CSQUEN if samec else constants.CIQUEN} "
                case constants.FKING:
                    fg = f"{constants.CSKING if samec else constants.CIKING} "
                case _:
                    fg = '  '
            con.print(f"{fg}", style=f"{fc} on {cc}", end='')
        con.print(f" {r+1}", style="bold cyan on black")
    con.print("  A B C D E F G H", style="bold cyan on black")
chess/constants.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""
Szoftverfesztelési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd

A chess package contants modulja.

A sakk megvalósításában használt konstansok:

F....  : A táblán egy figura
C...   : A táblán egy figura színe
CS.... : A konzolra kirajzolandó karakter, ha a mező és a figura színe egyezik
CI.... : A konzolra kirajzolandó karakter, ha a mező és a figura színe eltér
"""

FPAWN = 1
FKNGT = 2
FBSHP = 3
FROOK = 4
FQUEN = 5
FKING = 6

CLGT = 11
CDRK = 12

CSKING = '♔'
CSQUEN = '♕'
CSROOK = '♖'
CSBSHP = '♗'
CSKNGT = '♘'
CSPAWN = '♙'
CIKING = '♚'
CIQUEN = '♛'
CIROOK = '♜'
CIBSHP = '♝'
CIKNGT = '♞'
CIPAWN = '♟'
chess/figures.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""
Szoftverfesztelési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd

A chess package figures modulja.

A modul legfőbb feladata (egyelőre) az egyes bábukhoz tartozó helyes lépések ellenőrzése.
"""

from . import constants

"""
Az aktuális ellenfél színe.
"""
def opponent(ply):
    constants.CDRK if ply == constants.CLGT else constants.CLGT

"""
Egy mező fenyegetve van-e a ply játékos valamely bábuja által.
TODO
"""
def threatened_by(pos, ply):
    return False

"""
Sakktábla pozíció (pl. 'c6') koordinátákká konvertálása (pl. (2,5)).
"""
def p2c(pos):
    c, r = pos
    if 'a' <= c <= 'h':
        c = ord(c) - ord('a')
    else:
        raise ValueError(f"Wrong column: {c}")
    if '1' <= r <= '8':
        r = ord(r) - ord('1')
    else:
        raise ValueError(f"Wrong row: {r}")
    return c, r

"""
Koordináták (pl. 2, 5) sakktábla pozícióvá (pl. 'c6') konvertálása.
"""
def c2p(c, r):
    return f"{chr(c+ord('a'))}{chr(r+ord('1'))}"

"""
Segédfüggvény, egy szám előjele.
"""
def sign(val):
    return 1 if val > 0 else -1 if val < 0 else 0

"""
Gyalog mozgása helyes-e?
A világos gyalog 2->8 irányban, a sötét gyalog 7->1 irányban tud mozogni.
- A gyalog egyet léphet előre szabad mezőre.
- A kezdősorból (2. a világosnak és 7. a sötétnek) kettőt is előreléphet szabad mezőre, ilyenkor
  az ellenfélnek en-passant-ra van lehetősége az "átugrott", szintén üres mezőn, így ezt a mezőt kell visszaadni.
- A gyalog átlósan előre egyet lépve ütheti az ellenfél ott álló bábuját.
- En-passant lehetőség esetén a gyalog egyet átlósan előre lépve a megadott üres mezőre érkezhet.
"""
def valid_move_pawn(board, spos, epos, *, enpassant=None):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if board[sc][sr]['fig'] != constants.FPAWN:
        return False
    if sc == ec:
        if board[sc][sr]['col'] == constants.CLGT:
            if board[sc][sr+1]['fig'] is None:
                if er == sr+1:
                    return True
                if sr == 2 and er == 4 and board[sc][er]['fig'] is None:
                    return c2p(sc, 3)
        else:
            if board[sc][sr-1]['fig'] is None:
                if er == sr-1:
                    return True
                elif er == 7 and er == 5 and board[sc][er]['fig'] is None:
                    return c2p(sc, 6)
        return False
    if ec != sc-1 and ec != sc+1:
        return False
    if board[sc][sr]['col'] == constants.CLGT:
        if er == sr+1 and board[ec][er]['fig'] is not None and board[sc][sr]['col'] == constants.CDRK:
            return True
        return epos == enpassant
    else:
        if er == sr-1 and board[ec][er]['fig'] is not None and board[sc][sr]['col'] == constants.CLGT:
            return True
        return epos == enpassant
    return False

"""
Huszár mozgása helyes-e?
A huszár egyik irányba kettőt, másik irányba egyet mozdulva léphet vagy üthet, függetlenül attól, hogy
volt-e az útjában másik bábu.
"""
def valid_move_knight(board, spos, epos):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if board[sc][sr]['fig'] != constants.FKNGT:
        return False
    return ((abs(sc-ec) == 1 and abs(sr-er) == 2) or (abs(sc-ec) == 2 and abs(sr-er) == 1))

"""
A futó átlósan (mindig azonos színű mezőn) mozogva léphet vagy üthet, de bábut nem ugorhat át.
"""
def valid_move_bishop(board, spos, epos):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if board[sc][sr]['fig'] != constants.FBSHP:
        return False
    if abs(sc-ec) != abs(sr-er):
        return False
    dc, dr = sign(ec-sc), sign(er-sr)
    for i in range(1, abs(max(ec-sc, er-sr))):
        if board[sc + dc * i][sr + dr * i]['fig'] is not None:
            return False
    return True

"""
A bástya sorban vagy oszlopban egyenesen mozogva léphet vagy üthet, de bábut nem ugorhat át.
"""
def valid_move_rook(board, spos, epos):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if board[sc][sr]['fig'] != constants.FROOK:
        return False
    if sc != ec and sr != er:
        return False
    dc, dr = sign(ec-sc), sign(er-sr)
    for i in range(1, max(abs(ec-sc), abs(er-sr))):
        if board[sc + dc * i][sr + dr * i]['fig'] is not None:
            return False
    return True

"""
A vezér futóként vagy bástyaként, azaz átlósan, sorban vagy oszlopban mozoghat vagy üthet,
de bábut nem ugorhat át.
"""
def valid_move_queen(board, spos, epos):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if board[sc][sr]['fig'] != constants.FQUEN:
        return False
    if sc != ec and sr != er and abs(sc-ec) != abs(sr-er):
        return False
    dc, dr = sign(ec-sc), sign(er-sr)
    for i in range(1, abs(max(ec-sc, er-sr))):
        if board[sc + dc * i][sr + dr * i]['fig'] is not None:
            return False
    return True

"""
A kirány a 8 szomszédos mező valamelyikére léphet, ha azt nem fenyegeti az ellenfél egyetlen bábuja sem.
"""
def valid_move_king(board, spos, epos):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if board[sc][sr]['fig'] != constants.FKING:
        return False
    if abs(sc-ec) > 1 or abs(sr-er) > 1 or (sc == ec and sr == er):
        return False
    if threatened_by(epos, opponent(board[sc][sr]['col'])):
        return False
    return True

"""
A sáncoláskor, ha sem a király, sem a sáncoláshoz használt bástya nem mozdult még el
a kiinduló helyéről a játék során, és a kettő között minden mező üres, és sem a királyt,
sem a mellette az adott bástya irányában lévő két mezőt nem fenyegeti az ellenfél, akkor
A királyt két mezővel a bástya felé mozgathatjuk, a bástya pedig a királyt átugorva a
király által "átlépett" mezőre kerül.
"""
def valid_move_castling(board, spos, epos, *, king_moved=True, rook_a_moved=True, rook_h_moved=True):
    sc, sr = p2c(spos)
    ec, er = p2c(epos)
    if king_moved or rook_a_moved or rook_h_moved or sc != 5 or sr != 1 or sr != 8 or sr != er or board[sc][sr]['fig'] != constants.FKING or board[ec][sr]['fig'] is not None or threatened_by(c2p(ec, er), opponent(board[sc][sr]['col'])):
        return False
    if ec == 7:
        return board[6][sr]['fig'] is None and not threatened_by(c2p(6, sr), opponent(board[sc][sr]['col'])) and board[8][sr]['col'] == board[sc][sr]['col'] and board[8][sr]['fig'] == constants.FROOK
    elif ec == 3:
        return board[4][sr]['fig'] is None and not threatened_by(c2p(4, sr), opponent(board[sc][sr]['col'])) and board[1][sr]['col'] == board[sc][sr]['col'] and board[1][sr]['fig'] == constants.FROOK
    return False

Adott továbbá a csomaghoz pár kezdeti teszt:

test/conftest.py
1
2
3
4
5
6
7
8
"""
Szoftverfejlesztési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd
"""

import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
test/test_board.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
"""
Szoftverfesztelési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd

A chess package board moduljának egységtesztjei.
"""

import chess

def test_empty_board():
    table = chess.board.new_empty_table()
    for column in table:
        for field in column:
            assert field == {'fig': None, 'col': None}
test/test_figures.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
"""
Szoftverfesztelési folyamatok példaprogram
SZTE TTIK, Informatikai Intézet, Szoftverfejlesztés Tanszék
cc-by-nc-nd

A chess package figures moduljának egységtesztjei.
"""

import chess

def test_c2p():
    assert chess.figures.c2p(0, 0) == "a1"
    assert chess.figures.c2p(7, 7) == "h8"

def test_p2c():
    assert chess.figures.p2c("a8") == (0,7)
    assert chess.figures.p2c("h1") == (7,0)

def test_valid_move_rook_valid_free_col():
    table = chess.board.new_empty_table()
    table[3][3]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    assert chess.figures.valid_move_rook(table, 'd4', 'd6')

def test_valid_move_rook_valid_free_row():
    table = chess.board.new_empty_table()
    table[3][3]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    assert chess.figures.valid_move_rook(table, 'd4', 'b4')

def test_valid_move_rook_invalid_free():
    table = chess.board.new_empty_table()
    table[3][3]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    assert chess.figures.valid_move_rook(table, 'd4', 'g6') is False

def test_valid_move_rook_valid_capture():
    table = chess.board.new_empty_table()
    table[3][3]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    table[3][6]= {'fig': chess.constants.FROOK, 'col': chess.constants.CDRK}
    assert chess.figures.valid_move_rook(table, 'd4', 'd7')

def test_valid_move_rook_invalid_cannot_capture_self():
    table = chess.board.new_empty_table()
    table[3][3]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    table[3][6]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    assert chess.figures.valid_move_rook(table, 'd4', 'd7') is False

def test_valid_move_rook_invalid_blocked_path():
    table = chess.board.new_empty_table()
    table[3][3]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    table[3][6]= {'fig': chess.constants.FROOK, 'col': chess.constants.CLGT}
    assert chess.figures.valid_move_rook(table, 'd4', 'd8') is False

Szeretnénk, ha a main branch minden változása után lefutnának a tesztek, és a teszteredmények egy darabig elérhetőek maradnának. Mivel a test_figures.py használ egy olyan feature-t, amit a test_board.py-ban tesztelünk, ezért ha utóbbi hibával fut le, az előbbi futtatása már már felesleges. A fentieket (a git-okt.sed.inf.szte.hu szerveren a teljes SDP25-SzoftverfejlesztesiFolymatok csoportra éppen érvényes konfiguráció szerint legalábbis) a következő .gitlab-ci.yml konfigurációs fájl oldja meg:

.gitlab-ci.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# Teljes dokumentációhoz lásd: https://docs.gitlab.com/ci/yaml
# Itt definiáljuk a GitLab CI/CD pipeline lépéseit. A sorrend az fontos,
# abból a szempontból, hogy fentről lefele haladva lesznek végrehajtva a
# stagek. Egy stagen belül párhuzamos a végrehajtás.

stages:
  - test

# A "gép", "virtuális környezet", docker image megadása. Ez a default érték
# az egyes job-okban felülírható.

default:
  image: python:3.10.17

# Minden job indítása során a job konkrét utasításai előtt lefut.

before_script:
  - pip3 install -r requirements.txt

# Job-ok, feladatok
# * stage: melyik lépésben fut?
# * tags: melyik beállított runneren fusson?
# * only: mely referenciák (branch-ek) változása esetén fusson le?
# * script: a konkrét utasítások, amiket futtatni kell
# * allow_failure: hiba esetén továbbmegy (true) vagy leáll az egész folyamat (false)
# * artifacts: meghatározza a job által létrehozott artifact-eket, amiket megtarthatunk.
#      - Jelen esetben riportként feltöltjük őket.
test_board:
  stage: test
  tags:
    - test
  only:
    - main
  script:
    - python3 -m pytest --junitxml=./test_board.xml test/test_board.py
  allow_failure: false
  artifacts:
    when: always
    paths:
      - test_board.xml
    expire_in: "7 days"
    reports:
      junit: ./test_board.xml

test_figures:
  stage: test
  tags:
    - test
  only:
    - main
  script:
    - python3 -m pytest --junitxml=./test_figures.xml test/test_figures.py
  allow_failure: false
  dependencies:
    - test_board
  artifacts:
    when: always
    paths:
      - test_figures.xml
    expire_in: "7 days"
    reports:
      junit: ./test_figures.xml

Ehhez persze szükség lesz a requirements.txt fájlra is:

requirements.txt
1
2
3
4
5
6
7
8
iniconfig==2.1.0
markdown-it-py==3.0.0
mdurl==0.1.2
packaging==25.0
pluggy==1.5.0
Pygments==2.19.1
pytest==8.3.5
rich==14.0.0