जो भी कोई इम्यूलेटर बनाता है, वह रिग्रेशन से बचने के लिए स्वचालित परीक्षण से लगभग कभी नहीं बचता. एक आम तरीका यूनिट टेस्ट हैं। वे कोड के छोटे-छोटे हिस्सों को अलग से परखते हैं और इसलिए हर इम्यूलेटर के अनुरूप खास तौर पर लिखे जाने पड़ते हैं। इसके पूरक के रूप में इंटीग्रेशन टेस्ट पूरे इम्यूलेटर के व्यवहार को सत्यापित करते हैं।

इंटीग्रेशन टेस्ट के रूप में टेस्ट-ROMs

इंटीग्रेशन टेस्ट की एक खास दिलचस्प किस्म है टेस्ट-ROMs। ये सीधे एम्युलेटेड प्लेटफॉर्म पर चलते हैं और लक्षित व्यवहार की जाँच करते हैं। यूँ समझिए, वे हार्डवेयर को मानो अंदर से टेस्ट करते हैं।

एक गेम बॉय टेस्ट-ROM, उदाहरण के लिए, ये कर सकता है:

प्रसिद्ध टेस्ट सूट्स

गेम बॉय टेस्ट-ROMs की कई पूरी-की-पूरी कलेक्शन मौजूद हैं, जिन्हें टेस्ट-सूट्स कहा जाता है। सबसे प्रसिद्ध में से एक है Mooneye Test Suite, जिसे Joonas Javanainen (Gekkio) ने बनाया। इसी तरह Lior Halphon की SameSuite और Matt Currie के Mealybug Tearoom Tests भी जरूर देखने लायक हैं। इसके अलावा भी बहुत-सी सूट्स हैं, जिन्हें मैं यहाँ जगह के अभाव में अलग से सूचीबद्ध नहीं कर रहा।

अपने एक GitHub रिपॉज़िटरी में मैंने विभिन्न लेखकों के टेस्ट-सूट्स की एक संकलित सूची तैयार की है: Game Boy Test ROMs – Sammlung

यूनिट टेस्ट के मुकाबले टेस्ट-ROMs: फायदे और सीमाएँ

यूनिट टेस्ट की तुलना में गेम बॉय टेस्ट-ROMs की एंट्री-हर्डल काफ़ी ऊँची होती है। इंटीग्रेशन टेस्ट होने के नाते ये चलाने में स्वभावतः भारी होते हैं: पूरा इम्यूलेटर स्टार्ट करना, टेस्ट-ROM लोड कर के चलाना, और फिर किसी उपयुक्त तरीके से (जैसे स्क्रीन, मेमोरी या CPU रजिस्टर का मिलान) नतीजा निकालना पड़ता है। साथ ही टेस्ट-ROM बनाना भी जटिल है, क्योंकि अलग टूलचेन चाहिए, ROM अलग से कम्पाइल करने पड़ते हैं और ये आमतौर पर असेंबली में लिखे जाते हैं – जो बहुत प्रचलित भाषा नहीं है।

फिर भी टेस्ट-ROMs के स्पष्ट फायदे हैं:

  1. ये इम्यूलेटर की इम्प्लीमेंटेशन से स्वतंत्र होते हैं। प्लेटफॉर्म का कोई भी इम्यूलेटर इनसे टेस्ट किया जा सकता है।
  2. ये ओरिजिनल हार्डवेयर पर भी चलाए जा सकते हैं, और वहाँ कोई भी व्यक्ति इनके सही होने की पुष्टि कर सकता है।

गेम बॉय टेस्ट-ROMs का विकास

गेम बॉय टेस्ट-ROMs बनाने के लिए एक उपयुक्त टूलचेन की जरूरत होती है। मेरा पसंदीदा कॉम्बो है RGBDS और Makefile। RGBDS खास तौर पर गेम बॉय डेवलपमेंट के लिए बना है और असेंबलरलिंकर के साथ ग्राफिक्स कन्वर्ज़न और हेडर फिक्स जैसे टूल भी देता है। छोटे प्रोजेक्ट्स को rgbds-live के साथ सीधे ब्राउज़र में लिखकर आज़माया जा सकता है।

RGBDS के जाना-पहचाना विकल्प हैं WLA DX (कई CPUs के लिए क्रॉस-असेंबलर) और GBDK (गेम बॉय सॉफ्टवेयर के लिए C-कम्पाइलर)।

जहाँ RGBDS और WLA DX असेंबली में काम कराते हैं, वहीं GBDK C भी सपोर्ट करता है। गेम बॉय डेवलपमेंट में नए लोगों के लिए GBDK शायद आसान प्रवेश हो सकता है। हालाँकि C इस्तेमाल करने पर यह सीधे नियंत्रित नहीं रहता कि कम्पाइलर अंततः कौन-से CPU इंस्ट्रक्शन जनरेट करेगा। टेस्ट-ROMs में जहाँ अक्सर क्लॉक-टाइट टाइमिंग चाहिए, वहाँ असेंबली बेहतर विकल्प है।

वास्तविक हार्डवेयर पर चलाना

अपना सॉफ्टवेयर असली गेम बॉय पर चलाने के लिए एक उपयुक्त कार्ट्रिज-अडैप्टर चाहिए। मैं अभी EZ-FLASH Junior उपयोग करता हूँ, जिसे माइक्रो SD-कार्ड से भरा जा सकता है। ऐसे अन्य अडैप्टर भी अब आसानी से मिल जाते हैं। Google या Amazon पर “Game Boy Cartridge Adapter” सर्च करने से उपयुक्त विकल्प मिलेंगे।

लगभग सभी अडैप्टर एक साथ कई ROMs उपलब्ध कराते हैं। गेम बॉय बूट होने पर आप मेनू से मनचाहा ROM चुन लेते हैं।

लैपटॉप, माइक्रो SD-कार्ड, EZ‑Flash Junior और Game Boy Color पर चयन मेनू

लैपटॉप, माइक्रो SD-कार्ड, EZ‑Flash Junior और Game Boy Color पर चयन मेनू

एक सरल उदाहरण टेस्ट-ROM

समझाने के लिए हम DIV-टाइमिंग पर एक (बहुत) सरल टेस्ट-ROM देखते हैं। यह DIV-रीसेट के बाद पहली बार DIV इन्क्रीमेंट होने वाले क्लॉक-साइकिल को सत्यापित करता है और तीन हिस्सों में बँटा है:

  1. VRAM और BGP का इनिशियलाइज़ेशन
  2. असल टेस्ट का निष्पादन
  3. नतीजा दिखाना (✓ / ╳)

आज़माने के लिए नीचे दिया गया Listing 1 का कोड कॉपी करें और rgbds-live पर चलाएँ (वहाँ भरी हुई main.asm को पूरी तरह बदल दें)। टेस्ट को जान-बूझकर असफल कराने के लिए, जैसे लाइन 48 या 54 में संख्यात्मक मान बदल दें।

 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
; main.asm
;
; यह जान-बूझकर सरल रखा गया टेस्ट-ROM DIV-रीसेट के बाद
; पहली DIV-इन्क्रीमेंट का क्लॉक-साइकिल जाँचता है।

INCLUDE "hardware.inc"

SECTION "Header", ROM0[$100]
  jp EntryPoint

SECTION "EntryPoint", ROM0[$150]
EntryPoint:
  ; ----- 1. VRAM और BGP का इनिशियलाइज़ेशन
  ;
  ; LCD बंद करने के लिए V-Blank का इंतज़ार
: ldh a, [rLY]
  cp LY_VBLANK
  jr c, :-
  ; LCD बंद करें
  ld a, LCDC_OFF
  ldh [rLCDC], a
  ; VRAM इनिशियलाइज़ करें
  ld de, VramData
  ld hl, STARTOF(VRAM)
  ld bc, VramData.end - VramData
: ld a, [de]
  inc de
  ld [hli], a
  dec bc
  ld a, b
  or a, c
  jr nz, :-
  ; BG-पैलेट सेट करें
  ld a, %11_10_01_00
  ldh [rBGP], a

  ; ----- 2. टेस्ट चलाएँ
  ;
MACRO NOPS
  REPT \1
    nop
  ENDR
ENDM
  ; rDIV को पहली इन्क्रीमेंट से 1 M-साइकिल पहले पढ़ें
  ldh [rDIV], a ; rDIV रीसेट करें, काउंटर दोबारा शुरू
  NOPS 60
  ldh a, [rDIV]
  cp a, 0       ; पहली इन्क्रीमेंट से ठीक पहले rDIV == 0 होना चाहिए
  jr nz, .fail
  ; rDIV को इन्क्रीमेंट वाले M-साइकिल में पढ़ें
  ldh [rDIV], a ; rDIV रीसेट करें, काउंटर दोबारा शुरू
  NOPS 61
  ldh a, [rDIV]
  cp a, 1       ; पहली इन्क्रीमेंट पर rDIV == 1 होना चाहिए
  jr nz, .fail

  ; ----- 3. नतीजा दिखाएँ (✓ या ╳)
  ;
  ; टेस्ट सफल: ✓
  ld a, 1
  jr .finish
  ; टेस्ट असफल: ╳
.fail:
  ld a, 2
.finish:
  ; नतीजा दिखाएँ: ✓ या ╳
  ld [STARTOF(VRAM) + $1800], a
  ld a, LCDC_ON | LCDC_BLOCK01 | LCDC_BG_ON
  ldh [rLCDC], a
: jr :-

SECTION "VRAM data", ROM0
VramData:
  ; टाइल 0: खाली
  ds 16, 0
  ; टाइल 1: ✓
  dw `00000000
  dw `00000033
  dw `00000333
  dw `00000330
  dw `03303300
  dw `03333300
  dw `00333000
  dw `00033000
  ; टाइल 2: ╳
  dw `00000000
  dw `03300033
  dw `03330333
  dw `00333330
  dw `00033300
  dw `00333330
  dw `03330333
  dw `03300033
  ; बाकी VRAM
  ds $2000 - 3 * 16, 0
.end

सूची 1: rgbds-live पर आज़माने लायक एक सरल उदाहरण टेस्ट-ROM

टेस्ट-फ्रेमवर्क का इम्प्लीमेंटेशन

ऊपर वाले टेस्ट-ROM का बड़ा हिस्सा इनिशियलाइज़ेशन और नतीजा दिखाने में खर्च हुआ है। असल टेस्ट-लॉजिक करीब 100 में से सिर्फ़ 20 लाइनों में है। जब पूरी टेस्ट-सूट की बात आती है, तो यही बॉयलरप्लेट हर टेस्ट-ROM में दोहराना पड़ता है। इसलिए समझदारी यही है कि ये हिस्से एक साझा टेस्ट-फ्रेमवर्क में निकाल दिए जाएँ, जिसे सारे टेस्ट-ROMs शामिल करें।

आम तौर पर टेस्ट-ROM अपने टेस्ट-कोड से पहले फ्रेमवर्क-कोड को इनक्लूड करता है। फ्रेमवर्क प्रोग्राम-स्टार्ट और इनिशियलाइज़ेशन संभालता है और अक्सर इस्तेमाल होने वाली हेल्पर-रूटीन उपलब्ध कराता है। जिन टेस्ट-सूट्स का ज़िक्र हुआ — Mooneye Test Suite, SameSuite और Mealybug Tearoom Tests — सब यही तरीका अपनाते हैं।

टेस्ट-ROM में नतीजे का मूल्यांकन

अगर हम उदाहरण वाले टेस्ट-ROM से इनिशियलाइज़ेशन और नतीजा दिखाने का हिस्सा फ्रेमवर्क में निकाल दें, तो Listing 2 जैसा कोड बचता है। फ्रेमवर्क इनिशियलाइज़ेशन के बाद टेस्ट-ROM अपना टेस्ट सामान्य तरीके से चलाता है। नतीजे का मूल्यांकन भी टेस्ट-ROM के पास ही रहता है: गलती पर TestFail से एग्ज़िट (लाइन 16 और 23), और सफल होने पर TestSuccess (लाइन 26)।

 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
; main.asm

INCLUDE "test-framework.inc"

MACRO NOPS
  REPT \1
    nop
  ENDR
ENDM

  ; rDIV को पहली इन्क्रीमेंट से 1 M-साइकिल पहले पढ़ें
  ldh [rDIV], a ; rDIV रीसेट करें, काउंटर दोबारा शुरू
  NOPS 60
  ldh a, [rDIV]
  cp a, 0
  jp nz, TestFail

  ; rDIV को इन्क्रीमेंट वाले M-साइकिल में पढ़ें
  ldh [rDIV], a ; rDIV रीसेट करें, काउंटर दोबारा शुरू
  NOPS 61
  ldh a, [rDIV]
  cp a, 1
  jp nz, TestFail
  
  ; टेस्ट सफल
  jp TestSuccess

सूची 2: टेस्ट-फ्रेमवर्क के साथ उदाहरण टेस्ट-ROM का कोड

टेस्ट-फ्रेमवर्क (Listing 3) प्रोग्राम-स्टार्ट और इनिशियलाइज़ेशन के साथ TestSuccess और TestFail रूटीन देता है। यह नतीजा स्क्रीन पर दिखाने का काम भी संभालता है।

इस तरीके से हर टेस्ट छोटा और मेंटेन करने में आसान हो जाता है। साथ ही टेस्ट-ROM नतीजा निकालने के तरीके में पूरी तरह स्वतंत्र रहता है और जरूरत पर तुरंत टेस्ट रोक सकता है (fail fast)।

Mooneye Test Suite भी इसी तरह बनी है।

 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
; test-framework.inc

INCLUDE "hardware.inc"

SECTION "Header", ROM0[$100]
  jp EntryPoint

SECTION "Utilities", ROM0[$150]
; टेस्ट सफल: ✓ दिखाएँ
TestSuccess:
  ld a, 1 ; ✓
  jr Finish

; टेस्ट असफल: ╳ दिखाएँ
TestFail:
  ld a, 2 ; ╳
  jr Finish

Finish:
  ld [STARTOF(VRAM) + $1800], a
  ld a, LCDC_ON | LCDC_BLOCK01 | LCDC_BG_ON
  ldh [rLCDC], a
: jr :-

VramData:
  ; टाइल 0: खाली
  ds 16, 0
  ; टाइल 1: ✓
  dw `00000000
  dw `00000033
  dw `00000333
  dw `00000330
  dw `03303300
  dw `03333300
  dw `00333000
  dw `00033000
  ; टाइल 2: ╳
  dw `00000000
  dw `03300033
  dw `03330333
  dw `00333330
  dw `00033300
  dw `00333330
  dw `03330333
  dw `03300033
  ; बाकी VRAM
  ds $2000 - 3 * 16, 0
.end

SECTION "EntryPoint", ROM0
EntryPoint:
  ; LCD बंद करने के लिए V-Blank का इंतज़ार
: ldh a, [rLY]
  cp LY_VBLANK
  jr c, :-
  ; LCD बंद करें
  ld a, LCDC_OFF
  ldh [rLCDC], a
  ; VRAM इनिशियलाइज़ करें
  ld de, VramData
  ld hl, STARTOF(VRAM)
  ld bc, VramData.end - VramData
: ld a, [de]
  inc de
  ld [hli], a
  dec bc
  ld a, b
  or a, c
  jr nz, :-
  ; BG-पैलेट इनिशियलाइज़ करें
  ld a, %11_10_01_00
  ldh [rBGP], a

  ; इसके बाद असली टेस्ट कंट्रोल लेता है

सूची 3: उदाहरण टेस्ट-ROM द्वारा इस्तेमाल किया गया टेस्ट-फ्रेमवर्क

नतीजे का मूल्यांकन टेस्ट-फ्रेमवर्क में

हमारे उदाहरण टेस्ट-ROM ने सिर्फ़ दो डेटा-पॉइंट जाँचे: पहली इन्क्रीमेंट से ठीक पहले और ठीक उसी दौरान DIV-रजिस्टर का मान। ये सीधे टेस्ट-ROM में जाँचना आसान है। लेकिन जब एक टेस्ट सैकड़ों या हज़ारों डेटा-पॉइंट पैदा करे, तो टेस्ट-ROM में ही मिलान करना बोझिल हो जाता है।

एक विकल्प है कि मिलान का काम फ्रेमवर्क करे। इसके लिए फ्रेमवर्क WRAM में एक क्षेत्र आरक्षित करता है। हर टेस्ट-ROM वहाँ अपने वास्तविक मापे हुए मान लिखता है और अपेक्षित परिणाम भी दे देता है। फ्रेमवर्क दोनों की तुलना कर के सफलता/असफलता दिखा देता है।

फायदा यह कि टेस्ट-ROMs और भी पतले हो जाते हैं, क्योंकि उन्हें मूल्यांकन नहीं करना पड़ता।

कमज़ोरी यह कि पूरा टेस्ट चलकर ख़त्म होने के बाद ही नतीजा मिलता है। fail fast संभव नहीं, क्योंकि वह टेस्ट-ROM में ही मूल्यांकन माँगता है।

यही तरीका SameSuite और Mealybug Tearoom Tests जैसे सूट्स अपनाते हैं।

स्वचालित इंटीग्रेशन टेस्ट में उपयोग

उदाहरण वाले टेस्ट-ROM को स्वचालित इंटीग्रेशन टेस्ट की तरह चलाने के लिए इसे जाँच वाले इम्यूलेटर में लोड कर एक निर्धारित समय तक एम्यूलेट करना होगा। अंत में नतीजे की जाँच अपने-आप करनी होगी। इसे भरोसेमंद बनाने के लिए दो सवाल तय करने पड़ते हैं:

  1. टेस्ट-ROM कितनी देर चले, ताकि नतीजा तय हो जाए?
  2. टेस्‍ट-नतीजा अपने-आप कैसे जाँचा जाए — यानी सफलता या असफलता?

ये सवाल सिर्फ़ इस उदाहरण पर नहीं, हर टेस्ट-ROM पर लागू होते हैं।

प्रचलित कन्वेंशन

Mooneye Test Suite, SameSuite और Mealybug Tearoom Tests सब एक-सी कन्वेंशन मानते हैं। यह परंपरा मूलतः Mooneye GB इम्यूलेटर से आई मानी जाती है।

  1. कोई टेस्ट तब “समाप्त” माना जाता है, जब ld b, b इंस्ट्रक्शन चलाया जाए। चूँकि यह कुछ नहीं करता, सिवाय एक क्लॉक-साइकिल खाने के, सामान्य कोड में इसका इस्तेमाल नहीं होता।

  2. सफल टेस्ट CPU-रजिस्टरों में एक खास मान लिखकर संकेत देता है — फ़ाइबोनाची क्रम का एक अंश:

    • B = 3
    • C = 5
    • D = 8
    • E = 13
    • H = 21
    • L = 34

    किसी और मान के साथ ख़त्म होने वाला टेस्ट असफल माना जाएगा।

अगर हम टेस्ट-फ्रेमवर्क को Listing 4 के अनुसार बदलें, तो हमारा उदाहरण टेस्ट-ROM भी यही कन्वेंशन पूरी तरह निभाता है।

 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
; test-framework.inc

; < ... >

; टेस्ट सफल: ✓ दिखाएँ
TestSuccess:
  ld a, 1 ; ✓
  ld b, 3 ; CPU-रजिस्टर से "टेस्ट सफल" संकेत
  ld c, 5
  ld d, 8
  ld e, 13
  ld h, 21
  ld l, 34
  jr Finish

; टेस्ट असफल: ╳ दिखाएँ
TestFail:
  ld a, 2 ; ╳
  ld b, 0 ; CPU-रजिस्टर से "टेस्ट असफल" संकेत
  jr Finish

Finish:
  ld b, b ; "टेस्ट समाप्त" संकेत
  ld [STARTOF(VRAM) + $1800], a
  ld a, LCDC_ON | LCDC_BLOCK01 | LCDC_BG_ON
  ldh [rLCDC], a
: jr :-

; < ... >

सूची 4: टेस्ट-नतीजा सिग्नल करने के लिए फ्रेमवर्क में आवश्यक बदलाव

एनालॉग/दृश्य टेस्ट-नतीजे

हर टेस्ट-ROM अपने नतीजे खुद नहीं आँक सकता। उदाहरण के लिए वीडियो आउटपुट: टेस्ट-ROM चित्र तो बना सकता है, पर यह नहीं जाँच सकता कि वह सही दिख भी रहा है या नहीं।

ऐसे मामलों में टेस्ट-ROM बस ld b, b से बताता है कि टेस्ट पूरा हो गया। असल मूल्यांकन इंटीग्रेशन-टेस्ट चलाने वाला कोड करता है। इसके लिए टेस्ट-ROM के साथ एक रेफ़रेंस स्क्रीनशॉट दिया जाता है, जिसका इम्यूलेटर की असल आउटपुट से मिलान किया जाता है।

ऐसे स्क्रीनशॉट्स में रंगों के लिए भी एक प्रचलित कन्वेंशन है:

  • DMG स्क्रीनशॉट्स ये रंग इस्तेमाल करते हैं:
    #000000, #555555, #AAAAAA, #FFFFFF
  • CGB स्क्रीनशॉट्स में 15-बिट CGB रंगों को प्रति R/G/B 8-बिट में यूँ बदला जाता है:
    (X << 3) | (X >> 2)
  • “नॉन-CGB मोड” स्क्रीनशॉट्स में ये रंग होते हैं:
    • BGP: #000000, #0063C6, #7BFF31, #FFFFFF
    • OBP: #000000, #943939, #FF8484, #FFFFFF

निष्कर्ष

  • टेस्ट-ROMs इम्यूलेटरों के इंटीग्रेशन टेस्ट के लिए आदर्श हैं: वे एम्युलेटेड हार्डवेयर के व्यवहार को परखते हैं और इम्यूलेटर व ओरिजिनल हार्डवेयर — दोनों पर चलाए जा सकते हैं।
  • यूनिट टेस्ट की तुलना में इन्हें बनाना और चलाना अधिक मेहनत वाला है, लेकिन एक बार बन जाने पर इन्हें हर इम्यूलेटर में दोबारा इस्तेमाल किया जा सकता है।
  • सटीक टाइमिंग के लिए C की अपेक्षा असेंबली बेहतर है।
  • टेस्ट-फ्रेमवर्क बॉयलरप्लेट घटाता है और बड़ी टेस्ट-सूट्स बनाना-सम्भालना आसान करता है।
  • RGBDS, WLA DX या GBDK जैसे टूल्स और सही अडैप्टर (जैसे EZ-FLASH Junior) की मदद से विकास से लेकर असली हार्डवेयर पर टेस्ट तक पूरा रास्ता तय किया जा सकता है।