Debuggerin käyttö

Johdanto: ohjelman suorituksen seuraamisesta

Ohjelmoijan ja ohjelmoinnin opiskelijan on usein tarpeen selvittää, mitä jokin olemassa oleva ohjelma tekee. Esimerkiksi:

  • Pitää jatkokehittää aiemmin kehitettyä ohjelmaa.

  • Pitää kirjoittaa dokumentaatio toisen tekemästä ohjelmasta.

  • Pitää etsiä virhe väärin toimivasta ohjelmasta (itse kirjoitetusta tai toisen tekemästä).

  • Pitää tutustua opettajan antamaan esimerkkiohjelmaan.

Tällaisissa tilanteissa ohjelmoija voi käydä läpi ohjelman suoritusta mielessään vaiheittain. Vastaavaa pohdintaa tarvitaan myös uutta ohjelmaa kirjoittaessa: jotta ohjelmoija voisi arvioida, toimiiko hänen parhaillaan kirjoittamansa ohjelma, hänen täytyy kyetä "suorittamaan ohjelmaa päässään".

Lähestytään aihetta esimerkin kautta. Käytetään luvun 3.4 esittelemää esimerkkiä, Experience-luokkaa, joka on määritelty näin:

class Experience(val name: String, val description: String, val price: Double, val rating: Int):

  def valueForMoney = this.rating / this.price

  def isBetterThan(another: Experience) = this.rating > another.rating

  def chooseBetter(another: Experience) = if this.isBetterThan(another) then this else another

end Experience

Käytetään lisäksi tätä Experience-luokan metodeita kutsuvaa testiohjelmaa:

@main def testExperiences() =
  println("Aloitetaan ohjelman suoritus.")
  val wine1 = Experience("Il Barco 2001", "ookoo", 6.69, 5)
  val wine2 = Experience("Tollo Rosso", "turhahko", 6.19, 3)
  val better = wine1.chooseBetter(wine2)
  var result = better.name
  println(result)
  result = "Parempi kuin wine1? " + better.isBetterThan(wine1)
  println(result)

Käy läpi testExperiences-ohjelman suoritusta mielessäsi vaihe vaiheelta. Jos teet sen huolellisesti, huomaat, että kussakin vaiheessa on pidettävä mielessä monta asiaa:

  • Missä vaiheessa ohjelman suoritusta ollaan menossa? (Mikä rivi ja mikä rivin kohta?)

  • Mitkä ovat kunkin oleellisen luokan/yksittäisolion määrittelyn oleelliset osat? (Ne voi toki tarkistaa ohjelmakoodistakin.)

  • Mitkä ovat muuttujien arvot juuri kyseistä riviä suoritettaessa? Erityistä huolta vaativat var-muuttujat.

  • Mitkä ovat kuhunkin olioon liittyvät tiedot (ilmentymämuuttujien arvot)? Ne määräytyvät aiempien olionluontikäskyjen ja mahdollisten tilaan vaikuttavien käskyjen perusteella.

  • (metodia suoritettaessa:) Mikä on this-olio tässä yhteydessä? Mitkä ovat parametrimuuttujien arvot tässä metodikutsussa?

  • (metodista palatessa:) Mistä juuri tämä metodikutsu tehtiin? Mihin kohtaan koodia palataan?

  • Mitä oleellisia operaattoreita, funktioita ja luokkia on, ja mitä ne tekevät?

  • Mitä välituloksia on syntynyt moniosaisia lausekkeita evaluoidessa?

Kaikki näistä asioista eivät näy suoraan ohjelmakoodista. Koska muistettavaa on paljon, ohjelmoijat käyttävät monesti aputyökaluja, joka helpottaa yksityiskohtien hallintaa. Näin aivokapasiteettia vapautuu, ja ohjelmoija voi paremmin keskittyä miettimään esimerkiksi etsittävän virheen sijaintia tai tutkittavan ohjelman yleisiä tavoitteita ja toimintaperiaatteita.

Paperilappunenkin on varsin hyvä aputyökalu: voit kirjoittaa muistiinpanoja suorituksen vaiheista ja hahmotella kaavioita olioista ja muuttujien arvoista.

Paperin ja kynän lisäksi tai sijaan voi käyttää apuohjelmaa, joka kuvaa ohjelman suorituksen. Monia yllä luetelluista asioista onkin kurssimateriaalissa näytetty animaatioina. Kuitenkaan tällaisia animaatioita ei ole aina tarjolla, joten mikä neuvoksi?

Debuggereista

Debuggerit (debugger) ovat apuohjelmia, joilla voi tarkastella ohjelman toimintaa vaiheittain. Debuggerilla voi ajaa tallennettua ohjelmaa rivi riviltä ja tutkia samalla ohjelman tilaa. Erityisen hyödyllisiä debuggerit ovat virheiden eli "bugien" etsimisessä, mistä työkalun nimikin juontuu.

Debuggeri näyttää ohjelmasta pitkälti vastaavia asioita kuin kurssimateriaaliin upotetut animaatiotkin. Kuitenkaan tyypillinen debuggeri ei esitä ohjelman etenemistä yhtä graafisesti eikä yksityiskohtaisesti, koska debuggerit on tavallisesti suunniteltu kokeneiden ohjelmoijien eikä oppijoiden käyttöön ja koska suurten ohjelmien käsittelyyn tarvitaan erilainen esitystapa. Silti myös aloitteleva ohjelmoija voi hyötyä debuggerista.

Debuggeri voi olla erillinen työkalu tai integroidun työkaluston osa. Esimerkiksi IntelliJ tarjoaa debuggerin.

IntelliJ’n debuggeri

Debuggerin sujuva käyttö vaatii treeniä. Tässä vinkkejä alkuun pääsemiseen:

  • Silmäile läpi luettelo yleisimmistä IntelliJ’n debuggerin toiminnoista alla.

  • Tee pieni harjoitus alempana tällä sivulla. Hyödynnä mainittuja toimintoja.

  • Kokeile debuggeria omin päin: kirjoita pieni testiohjelma ja käsittele sitä debuggerissa tai tutki kurssin esimerkkiohjelmia.

  • Etsi lisämateriaalia netistä. Esimerkiksi tässä YouTube-videossa esitellään lyhyesti debuggerin ominaisuuksia (enemmänkin kuin kurssilla tarvitset) ja IntelliJ’n sivustolla selitetään debuggerin käyttöä (esimerkkinä Java-koodia käyttäen, mutta samat toiminnot toimivat Scalaankin).

  • Pyydä assareilta apua harjoitusryhmissä.

Luettelo tärkeimmistä debuggeritoiminnoista

../_images/ij_debugger_breakpoint.png

Punapallura rivin marginaalissa merkitsee keskeytyskohdan.

Breakpointin eli keskeytyskohdan asettaminen

Ennen kuin aloitat ohjelman tutkimisen debuggerissa, on yleensä syytä asettaa ainakin yksi keskeytyskohta (breakpoint). Keskeytyskohta on ohjelman kohta, jossa debuggeri pysäyttää suorituksen ja antaa sinun tutkia ohjelman tilaa. Mieti ensin, mille riville haluat keskeytyskohdan. (Voit valita itse, mutta jos emmit, valitse kokeeksi vaikka ensimmäinen käsky käynnistysolion tai käynnistysfunktion sisällä.) Merkitse keskeytyskohta siirtämällä kursori tuolle riville ja valitsemalla Run → Toggle Breakpoint tai painamalla Ctrl+F8. Vielä yksinkertaisemmin tuo käy, kun klikkaat palkkia rivin ja rivinumeron välissä. Samalla tavalla voit poistaa olemassa olevan keskeytyskohdan. Punainen pallura koodin marginaalissa kertoo siinä olevan keskeytyskohdan.

Ohjelman käynnistäminen debuggerissa

Aja käyttäen Run-komennon sijaan Debug-komentoa. Se löytyy esimerkiksi valitsemalla käynnistystiedosto ja sitten joko sen oheisvalikko tai yläpalkin Run-valikko, joka sisältää myös Debug-käskyn; työkalupalkin ludekin lude käy. Ohjelma käynnistyy ja suorittuu (ensimmäiseen) keskeytyskohtaan saakka (tai kokonaan, jos keskeytyskohtia ei ole).

Eteneminen suorittamalla rivi kokonaan

Run → Debugging Actions → Step Over tai paina F8 (tai vastaava ikoni Debug-osiosta). Kone suorittaa koko rivin näyttämättä välivaiheita. Erityisesti: jos rivillä on funktiokutsu, sen suorituksen vaiheita ei esitellä.

Eteneminen näyttäen välivaiheetkin

Run → Debugging Actions → Step Into tai paina F7. Tämä käsky toimii kuten edellinen paitsi, että myös (tietynlaiset) välivaiheet näytetään ja koko rivin suorittaminen voi siis vaatia lukuisia peräkkäisiä Step Into-komentoja. Erityisesti: jos valitulla rivillä on funktiokutsu, niin Step Into "hyppää sen sisään". Jos rivillä on useita funktiokutsuja, IntelliJ pyytää sinua valitsemaan nuolinäppäimillä, minkä sisään askellat. (Se saattaa tarjota vaihtoehdoksi myös kirjastofunktioita kuten println, joiden sisään askeltaminen ei kuitenkaan yleensä ole hyödyllistä.)

Ohjelman tilan tutkiminen

Näet tietoja muuttujista Debug-osiosta, joka tulee näkyviin IntelliJ’n alareunaan kun käynnistät debuggerin.

Palaaminen funktiokutsun sisältä riville, josta sitä kutsuttiin

Run → Debugging Actions → Step Out tai paina Shift+F8. Funktio, jossa oltiin, tulee samalla suoritetuksi loppuun.

Suorituksen jatkaminen ilman askellusta

Run → Debugging Actions → Resume tai paina F9. Tämä suorittaa ohjelman seuraavaan keskeytykseen saakka tai loppuun, jos vastaan ei tule keskeytyskohtia.

Peruuttaminen

Tälle ei ole komentoa, mutta voit lopettaa suorituksen ja aloittaa sen alusta.

Lopettaminen

Keskeytä esimerkiksi valitsemalla Run → Stop tai painamalla Ctrl+F2 tai suorita ohjelma loppuun yllä mainituilla etenemiskomennoilla.

Monille mainituista debuggerikomennoista löytyy myös kuvakkeet Debug-näkymän reunoista. Esimerkiksi Stop-komennon voi antaa myös stop-nappulalla.

../_images/ij_debugger_debugpanel-fi.png

IntelliJ’n Debug-näkymä. Sen Debugger-välilehti, joka näkyy tässä kuvassa, näyttää kutsupinon vasemmalla ja muuttujat arvoineen oikealla. Tarjolla on myös Console-välilehti tulosteineen (joka ei ole tässä esillä). Debuggeria voi ohjata yläreunan kuvakkeilla; niillä on kätevät näppäimistövastineet, kuten yllä kuvattiin.

Kysyttyä: Miksei debuggeri osaa mennä takaperin?

Taaksepäin askellus on teknisesti vaativampaa, eivätkä useimmat työkalut tue sitä. Mutta ei sekään mahdotonta ole, ja taitaa olla vähän yleistymään päin.

Debuggeriharjoitus

Nouda käyttöösi projekti Miscellaneous. Se sisältää luokan nimeltä o1.excursion.Excursion ja sitä käyttävän ohjelman testExcursion eri tiedostossa.

Lue ensin

Tutustu Excursion-luokan dokumentaatioon ja silmäile vähän ohjelmakoodiakin. Jatka vasta sitten.

Poraudu annetun ohjelman toimintaan debuggerilla. Harjoituksen aikana tulee osoittautumaan, että Excursion-luokassa on pieni virhe.

Koska debuggerin käyttö voi olla aluksi hämmentävää, saatat haluta kokeilla tätä harjoitusryhmässä, jossa voit pyytää assistentilta neuvoa!

Seuraavat ohjeet ovat suuntaa antavia. Saa soveltaa; melkeinpä pitää.

Ekskursio-ohjelma debuggerissa

Aseta keskeytyskohta testExcursion-metodin riville, jolla on ensimmäinen println-käsky.

Käynnistä ohjelma debuggerissa.

Suoritus pysähtyy keskeytyskohtaan. Korostettuna on tulostuskäskyn sisältävä rivi. Etene ohjelman suoritusta vähitellen tutkien:

  • Paina Step Over eli F8 suorittaaksesi koko korostetun rivin. Korostus siirtyy seuraavalle riville.

  • Ota alhaalta Debug-osiosta esiin Console-välilehti. Siellä näkyy nyt ohjelman osittainen tuloste: ensimmäinen tulostuskäsky on suoritettu.

Seuravaaksi suoritettavana on runFactoryScenario()-funktiokutsu. Älä kuitenkaan tällä kertaa suorita koko funktiota kerralla painamalla Step Over. Tutkitaan sen sijaan tuon funktion sisältöä vaiheittain:

  • Paina Step Into F7. Suoritus siirtyy kutsutun funktion alkuun.

  • Huomaa: Debug-osion Debugger-kohdassa näkyy kutsupinon kehysten→ luettelo. Nyt olet runFactoryScenario-funktiossa, jota on kutsuttu testExcursionista.

Etene funktiossa vähitellen omaan tahtiisi:

  • Käytä muutamalle ensimmäiselle riville Step Over-komentoa F8.

  • Koeta ymmärtää jokainen tapahtuva askel. Katso, miten tuloste muodostuu vähitellen Console-välilehdelle.

  • Pidä silmällä muuttujien arvoja Debugger-osion luettelossa. Erityisesti: testTrip viittaa Excursion-olioon, jolla on puolestaan omat muuttujansa.

  • Kokeile myös Step Into-komentoa F7 askeltaaksesi esimerkiksi registerInterest-metodin sisään.

    • Jos jossain vaiheessa päädyt oudosti Predef-nimisen olion toteutukseen tai muuhun scala-nimisen pakkauksen koodin, se tarkoittanee, että olet edennyt Step Intolla Scalan println-käskyn toteutuksen sisään, mikä tuskin on se, mitä halusit. Ei hätää. Voit joko palata takaisin Step Out -käskyllä Shift+F8 tai vaikka aloittaa alustakin.

  • Pidä silmällä Debugger-osion kutsupinoa ja sen muuttumista metodikutsujen alkaessa ja päättyessä.

  • Tutki ohjelman suoritusta, kunnes jossain vaiheessa Step Into- tai Step Over-komennon yhteydessä huomaat, että tapahtuu jotakin erilaista:

Katosiko kutsupino Debugger-kohdasta ja muuttujat arvoineen siitä vierestä? Niin pitikin. Tutkitaan:

  • Ohjelma-ajo katkesi. Silmäys Console-osioon kertoo, että on syntynyt virhetilanne nimeltä IndexOutOfBoundsException.

  • Virheen nimen alla näkyy, että kyse on ekskursioluokan lastParticipant-metodista, joka on kutsunut ArrayBuffer-luokan koodia. (ArrayBuffer on luokka jota on käytetty Scalan puskurien toteutuksessa.)

  • Äsken suoritettu koodirivi "heitti" (throw) virheen, joka kaatoi ohjelmamme.

Olet jo ehkä huomannut — ja virheilmoituksesta voit varmistaa — että virhe aiheutui, kun suoritettiin sitä lastParticipant-metodin kutsua, joka tehtiin test.scala-tiedoston rivillä 26.

Tutki tätä metodikutsua tarkemmin debuggerissa. (Tee se kokeeksi, vaikka olisitkin jo keksinyt, missä vika piilee.) Voit toimia vaikkapa seuraavasti.

Virhetilanteen tutkiskelua

Käynnistä taas debuggeri. Suoritus keskeytyy aiemmin asettamaasi keskeytyskohtaan.

Lisää uusi keskeytyskohta riville 26.

Anna Resume-komento F9. Nyt jatkettiin juuri asettamaasi toiseen keskeytyskohtaan saakka. Tätä virhetilanteen aiheuttavaa riviä ei vielä suoritettu.

Valitse Step Into F7 ja vaihtoehdoista lastParticipant (eikä println). Päädyt valitun metodin sisään.

Ei välitetä tällä kertaa numberOfInterested- ja numberOfParticipants-metodien sisäisestä toteutuksesta vaan askelletaan niiden yli. Paina Step Over F8 pari kertaa ohittaaksesi if-rivin ja val-rivin yksityiskohdat. Nuo rivit eivät vielä kaada ohjelmaa.

Tutki Debug-näkymästä löytyviä Excursion-olion tietoja. (Muuttujaluettelossa this viittaa tuohon olioon sinä aikana, kun olemme suorittamassa tuon olion lastParticipant-metodia.) Löydät sieltä esimerkiksi interestedStudents-muuttujan osoittaman puskurin ja sen sisältä neljä ilmoittautuneen henkilön nimeä sekä merkinnän puskurin tämänhetkisestä koosta (4).

Huomaa myös paikallinen muuttuja numberOfLast.

Palauta mieleen: virheen tyyppi on IndexOutOfBoundsException eli "indeksi on rajojen ulkopuolella". Huomaatko virheen lastParticipant-metodissa? Päättele koodin perusteella ja tutki tilannetta halutessasi lisää debuggerissa.

Jos suoritat vielä seuraavankin Some-alkuisen rivin, ohjelma kaatuu.

Mikä kahden merkin (plus mielellään kahden välilyönnin) kokoinen lisäys korjaa virheen?

Miten sijoittaa keskeytyskohdat?

Äsken asetimme keskeytyskohdat käynnistysolioon. Olisimme voineet myös asettaa keskeytyskohdan suoraan Excursion-luokan metodiin lastParticipant. Tällöin keskeytettäisiin aina, kun tuota metodia kutsutaan.

Vastaavasti graafisella käyttöliittymällä varustetussa ohjelmassa voit asettaa keskeytyskohdan vaikkapa tapahtumankäsittelijämetodiin tai johonkin niistä mallin metodeista, joiden metodeja tapahtumankäsittelijä kutsuu. Näin suoritus keskeytyy kyseisen metodin aktivoituessa.

On myös mahdollista asettaa keskeytyskohta, joissa suoritus katkeaa vain muuttujien arvojen ollessa tietynlaiset. Kokeile napsauttaa punaista keskeytyskohtapalleroa hiiren oikealla napilla ja tutki. Ja sellaisenkin keskeytyskohdan voi asettaa, joka katkaisee ohjelman suorituksen, kun suorituksen aikana tapahtuu virhe (Run → View Breakpoints → Java Exception Breakpoints).

Valitettava rajoitus (IntelliJ’n Scala-debuggerissa)

Nykyisessä IntelliJ’n Scala-debuggerissa on rajoitus: se ei toimi kunnolla kaikille käynnistysolioille (App). Esimerkkikoodi:

object TroubleWithIJDebugger extends App:

  println("Starting")
  myMethod()
  println("Called myMethod")
  myMethod()
  println("Done")

  def myMethod() =
    println("Hello from myMethod")
    println("Now let's go back to the main code")

end TroubleWithIJDebugger

Debuggeri reistailee, jos seuraavat kolme ehtoa täyttyvät:

Haluat tutkia vaihe vaiheelta koodia, joka on kirjoitettu suoraan käynnistysolion (App) sisään (siis ei metodiin);

tuosta koodista kutsutaan metodeita, joiden sisään haluat askeltaa; ja

haluat palata kutsutuista metodeista takaisin suoraan App-olion sisällä olevaan koodiin ja jatkaa sen parissa.

Onneksi tuo rajoitus on helppo kiertää järjestämällä koodi toisin. Kumpi vain seuraavista toimii debuggerissa nätisti:

object HelperMethod extends App:

  actuallyDoStuff()

  def actuallyDoStuff() =   // debugattava toiminta on metodissa
    println("Starting")
    myMethod()
    println("Called myMethod")
    myMethod()
    println("Done")

  def myMethod() =
    println("Hello from myMethod")
    println("Now let's go back to the main code")

end HelperMethod
@main def mainFunctionInsteadOfApp() =

  println("Starting")
  myMethod()
  println("Called myMethod")
  myMethod()
  println("Done")

  def myMethod() =
    println("Hello from myMethod")
    println("Now let's go back to the main code")

end mainFunctionInsteadOfApp

Mainittu rajoitus poistunee tulevissa IntelliJ’n versioissa, mutta toistaiseksi näin. Rajoituksesta ei tarvitse piitata, kun debuggaat muunlaisia ohjelmia, joissa ei ole tarpeen tehdä useita metodikutsuja käynnistysoliosta sinne aina välillä palaten.

Lopuksi: debuggereista ja debuggaamisesta

Tämän kurssin tehtävissä sinua ei yleensä erikseen käsketä käyttämään debuggeria. Toivottavasti silti omaksut tämä työkalun arsenaaliisi vähitellen. Älä unohda, että sinulla on debuggeri käytettävissäsi, kun huomaat laatimasi ohjelman toimivan väärin.

Kuten olet nähnyt, niin nimestään huolimatta debuggeri ei ole taikakalu, joka korjaa ohjelmista virheitä. Se ei "edes" etsi niitä. Näihin tehtäviin tarvitaan ihmistä. Silti jo se, että debuggeri auttaa ihmistä käymään läpi ohjelmakoodin suoritusta, on joskus kullanarvoinen apu.

Ihmistä tarvitsevan debuggaustyön väistämättömyyteen liittyy myös tämä vajaan minuutin video, jossa Steve Jobs mainostaa ohjelmoijan arkea helpottavaa ominaisuutta eräässä IDE:ssä.

Yhteenvetoa

  • Ohjelmoijan on kyettävä "suorittamaan ohjelmaa päässään" vaihe vaiheelta.

  • Tekniset apuvälineet tukevat ohjelman suoritusvaiheiden selvittämistä erityisesti silloin, kun ohjelma on tuntematon, virheellinen tai monimutkainen.

  • Debuggeri on apuohjelma, jonka välityksellä ihminen voi käydä läpi ohjelman suorituksen vaiheita ja tarkastella ohjelman tilaa.

  • IntelliJ tarjoaa debuggerin. Siitä voi olla hyötyä tällä kurssilla ja muutenkin.

  • Termejä sanastosivulla: debuggeri, keskeytyskohta.

Palaute

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Lisäkiitokset tähän lukuun

Kiitokset Jobs-videota suositelleelle aiemmalle kurssinkävijälle.

a drop of ink
Palautusta lähetetään...