Skip to content

laskari 4

Matti Luukkainen edited this page Apr 18, 2016 · 21 revisions
Tehtävien palautuksen deadline su 17.4. klo 23.59

Ohjausta tehtävien tekoon to 14.4. 14-17 B221

palautetaan GitHubin kautta

  • palautusta varten voit käyttää samaa repoa kuin aiemman viikon palautuksissasi
  • palautusrepositorion nimi ilmoitetaan tehtävien lopussa olevalla palautuslomakkeella

1. lisää mavenia: pom.xml

Maven-projekti konfiguroidaan projektin juuressa olevassa pom.xml-tiedostossa.

Tutkitaan hieman viime viikon tehtävissä 3-5 käytetyn projektin eli repositorion https://github.com/mluukkai/ohtu2016 hakemistossa viikko4/LoginWeb2 olevan projektin pom.xml:in sisältöä.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>LoginEasyBv1</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>LoginEasyBv1</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <easyb.version>1.5</easyb.version>
        <cobertura.version>2.4</cobertura.version>
        <org.springframework.version>3.1.1.RELEASE</org.springframework.version>
    </properties>

    <dependencies>
        
        <!-- testing -->
        
        <dependency>
            <groupId>org.easyb</groupId>
            <artifactId>easyb-core</artifactId>
            <version>${easyb.version}</version>
            <scope>test</scope>        
        </dependency>
        
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit-dep</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
            
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-all</artifactId>
            <version>1.1</version>
            <scope>test</scope>
        </dependency>
        
         <!-- SPRING -->
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>        
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>                        

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>   
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>cobertura-maven-plugin</artifactId>
                <version>${cobertura.version}</version>
                <configuration>
                    <formats>
                        <format>html</format>
                        <format>xml</format>
                    </formats>
                </configuration>
            </plugin>
                                  
            <plugin>
                <groupId>org.easyb</groupId>
                <artifactId>maven-easyb-plugin</artifactId>
                <version>1.4</version>
                <executions>
                    <execution>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <storyType>html</storyType>
                    <storyReport>${basedir}/target/easyb/easyb-report.html
                    </storyReport>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Alussa määritellään projektin tiedot (nimi, versio.)

Kohdassa properties määritellään mm. alempana käytettäviä vakioita

Maven osaa ladata riippuvuuksia (eli käytännössä jar-tiedostoja) automaattisesti oletusrepositorioista. Kaikki riippuvuudet eivät kuitenkaan löydy oletusrepositorioista ja tälläisiä tilanteita varten osaan repositories voi määritellä vaihtoehtoisia repositorioita joista maven voi etsiä riippuvuuksia.

Riippuvuudet määritellään osassa dependencies

  • alussa olevien riippuvuuksien (mm. easyb, junit) scope on test, tämä tarkoittaa että ne ovat käytössä vain testeissä
  • selenium-riippuvuuden scope on compile, tällöin selenium on käytössä testeissä ja normaalissa koodissa
  • jos ohjelmassa tarvitaan jar:eja, tulee niitä vastaavat maven-riippuvuudet kirjata dependencies-osaan, riippuvuuksia voi etsiä mm. seuraavista: http://search.maven.org tai http://mvnrepository.com/

Osassa build määritellään kääntämiseen liittyvien pluginien toimintaa

  • kääntämisessä määritellään käytettävän javan versiota 1.6, tämä tapahtuu maven-compiler-plugin:ia konfiguroimalla
    • jos tätä konfiguraatiota ei tehdä käyttää compiler-plugin oletusarvoista javan versiota. maven 3:ssa se on 1.6 mutta maven 2.*:ssa versio 1.3
  • seuraavaksi määritellään, että cobertura-plugin tuottaa raporttinsa html:nä ja xml:nä
  • jetty-pluginiin liittyy enemmänkin konfiguraatioita
    • kohdan executions-alla määritellään, että jetty (eli sovelluksen käyttämä maven-projektiin integroitu web-palvelin) käynnistetään vaiheessa pre-integration-test ja sammutetaan vaiheessa post-integration-test, tämä saa aikaan sen, että kun ajetaan integraatiotestejä, eli suoritetaan komento mvn integration-test, on jetty päällä testien ajamisen aikana
  • easyb-pluginin määritellään ajavan testit integration-test-vaiheessa

2. lisää mavenia: riippuvuuksien lisääminen

Hae repositorion https://github.com/mluukkai/ohtu2016 hakemistossa viikko4/TyhjaProjekti lähes tyhjän maven-projektin runko.

  • mukana on kohta tarvitsemasi luokka Submission

Tehdään ohjelma jonka avulla voit lukea kurssilla palauttamiesi tehtävien statistiikan osoitteesta http://ohtustats2016.herokuapp.com/

Omat palautukset palauttava sivu on http://ohtustats2016.herokuapp.com/students/012345678/submissions (vaihda 012345678 omaksi opiskelijanumeroksesi). Palvelin palauttaa tietosi json-muodossa

Tavoitteena on tehdä ohjelma, joka ottaa komentoriviparametrina opiskelijanumeron ja tulostaa palautettujen tehtävien statistiikan ihmisystävällisessä muodossa.

Ohjelmassa tarvitaan muutamaa kirjastoa:

Liitä projektisi pom.xml:n seuraavat riippuvuudet

  • commons-httpclient, Commons IO, gson
  • löydät riippuvuuksien tiedot osoitteesta http://mvnrepository.com/
  • Ainakin seuraavat versiot on todettu yhteensopiviksi ja toimivaksi projektin koodin kanssa: commons-httpclient 3.1, Commons IO 2.0, gson 2.1

Ota mallia edellisen tehtävän projektista ja määrittele maven-compiler-plugin käyttämään javan versiota 1.6

Voit ottaa projektisi pohjaksi seuraavan tiedoston:

import com.google.gson.Gson;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.io.IOUtils;

public class Main {

    public static void main(String[] args) throws IOException {
        String studentNr = "012345678";
        if ( args.length>0) {
            studentNr = args[0];
        }

        String url = "http://ohtustats2016.herokuapp.com/students/"+studentNr+"/submissions";

        HttpClient client = new HttpClient();
        GetMethod method = new GetMethod(url);
        client.executeMethod(method);

        InputStream stream =  method.getResponseBodyAsStream();

        String bodyText = IOUtils.toString(stream);

        System.out.println("json-muotoinen data:");
        System.out.println( bodyText );

        Gson mapper = new Gson();
        Submission[] subs = mapper.fromJson(bodyText, Submission[].class);
        
        System.out.println("Oliot:");
        for (Submission submission : subs) {
            System.out.println(submission);
        }

    }
}

HUOM: jos teet koodia NetBeansilla, kirjastoja ei ehkä tunnisteta ennenkuin teet clean and buildin ja NB lataa ne mavenin repositoriosta koneellesi.

Tehtäväpohjassa on valmiina luokan Submission koodin runko. Gson-kirjaston avulla json-muotoisesta datasta saadaan taulukollinen Submission-olioita, joissa jokainen olio vastaa yhden viikon palautusta. Tee luokkaan oliomuuttuja (sekä tarvittaessa getteri ja setteri) jokaiselle json-datassa olevalle kentälle, jota ohjelmasi tarvitsee. Kentät a1, a2 jne vastaavat viikolla tehtyjä yksittäisiä tehtäviä.

Tee kuitenkin ohjelmastasi tulostusasultaan miellyttävämpi, esim. seuraavaan tyyliin:

opiskelijanumero 012345678

 viikko 1: tehtyjä tehtäviä yhteensä: 9, aikaa kului 3 tuntia, tehdyt tehtävät: 1 2 3 4 5 6 7 9 11 
 viikko 2: tehtyjä tehtäviä yhteensä: 6, aikaa kului 4 tuntia, tehdyt tehtävät: 1 2 3 6 7 8  

yhteensä: 15 tehtävää 7 tuntia

3. lisää mavenia: jar joka sisältää kaikki riippuvuudet

  • tehdään äskeisen tehtävän projektista jar-tiedosto komennolla mvn install
  • suoritetaan ohjelma komennolla java -cp tiedostonNimi.jar ohtu.Main
  • mutta ohjelma ei toimikaan, tulostuu:
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/httpclient/HttpMethod
Caused by: java.lang.ClassNotFoundException: org.apache.commons.httpclient.HttpMethod
at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
Could not find the main class: ohtu.Main.  Program will exit.

Mistä on kyse?

  • ohjelman riippuvuuksia eli projekteja commons-httpclient, Commons IO ja gson vastaavat jar-tiedostot eivät ole käytettävissä, joten ohjelma ei toimi
  • saamme generoitua ohjelmasta jar-tiedoston joka sisältää myös riippuvuudet mavenin assembly-pluginin avulla
  • lisää pom.xml:n plugineihin seuraava:
<build>
        <plugins>
 
            <!-- ... -->
 
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.2.1</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
 
        </plugins>
 
    </build>

komennolla mvn assembly:assembly syntyy koko ohjelman sisältävä "standalone"-jar-tiedosto:

$ java -cp TyhjaProjekti2-1.0-jar-with-dependencies.jar ohtu.Main 012345678
 
opiskelijanumero 012345678

 viikko 1: tehtyjä tehtäviä yhteensä: 9, aikaa kului 3 tuntia, tehdyt tehtävät: 1 2 3 4 5 6 7 9 11 
 viikko 2: tehtyjä tehtäviä yhteensä: 6, aikaa kului 4 tuntia, tehdyt tehtävät: 1 2 3 6 7 8  

yhteensä: 15 tehtävää 7 tuntia

Riippuvuudet sisältävä jar-voidaan myös tehdä käyttämällä mavenin shade-pluginia Shade-pluginin avulla saadaan itseasiassa aikaan "helppokäyttöisempi" jar, joka voidaan käynnistää määrittelemättä main-metodin sisältävää luokkaa.

Määrittele shade-pluginille mainClassin sijainti lisäämällä pom.xml:ääsi seuraava:

<build>
        <plugins>
 
            <!-- ... -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>1.6</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>ohtu.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
 
    </build>

Saat luotua jar:in komennolla mvn package, ja ohjelman suoritus tapahtuu komennolla java -jar tiedostonnimi.jar

4. git: monta etärepositorioa

Tehtävässä oletetaan, että sinulla on 2 repositoria GitHub:issa. Käytetään niistä nimiä A ja B

  • kloonaa A koneellesi
  • liitä B paikalliselle koneelle kloonaamaasi repositorioon etärepositorioksi
  • nyt paikallisella repositoriollasi on kaksi remotea A (nimellä origin) ja B (määrittelemälläsi nimellä)
    • tarkista komennolla git remote -v että näin todellakin on
  • pullaa B:n master-haaran sisältö paikalliseen repositorioon (komennolla git pull beelleantamasinimi master)
    • pullaus siis aiheuttaa sen, että etärepositorin master-haara mergetään lokaalin repositorion masteriin, tämä voi aiheuttaa konfliktin, jos, niin ratkaise konflikti!
  • pushaa paikallisen repositorion sisältö A:han eli originiin

5. git: monta etärepositorioa jatkuu

jatketaan edellistä

  • liitä paikalliseen repositorioosi (edellisen tehtävän A) remoteksi repositorio git://github.com/mluukkai/ohtu2016.git esim. nimellä ohtu
  • ei pullata ohtu-repossa olevaa tavaraa lokaaliin, vaan tehdään sille oma träkkäävä branchi:
    • anna komennot git fetch ohtu ja git checkout -b ohtu-lokaali ohtu/master
    • varmista komennolla git branch että branchi (nimeltä ohtu-lokaali) syntyi ja että olet branchissa
    • tee komento ls niin näet, että olet todellakin ohtu-repon lokaalissa kopiossa
  • siirretään (tai otetaan mukaan, alkuperäinen ei häviä) ohtu-lokaali:n hakemisto viikko2 paikallisen repon masteriin:
    • palaa master-branchiin komennolla git checkout master
    • liitä branchin ohtu-lokaali hakemisto viikko2 masteriin komennolla git checkout ohtu-lokaali viikko2
    • varmista että hakemisto on nyt staging-alueella komennolla git status
    • committaa
    • nyt sait siirrettyä sopivan osan toisen etärepositorion tavarasta lokaaliin repositorioon!
  • pushaa lokaalin repositorion sisältö sekä originiin että B:hen

6. git: tägit

Tee tämä tehtävä repositorioon, jonka palautat

  • Lue http://git-scm.com/book/en/Git-Basics-Tagging (kohdat signed tags ja verifying tags voit skipata)
  • tee tägi nimellä tagi1 (lightweight tag riittää)
  • tee kolme committia (eli 3 kertaa muutos+add+commit )
  • tee tägi nimellä tagi2
  • katso gitk-komennolla miltä historiasi näyttää
  • palaa tagi1:n aikaan, eli anna komento git checkout tagi1
    • varmista, että tagin jälkeisiä muutoksia ei näy
  • palaa nykyaikaan
    • tämä onnistuu komennolla git checkout master
  • lisää tägi edelliseen committiin
    • onnistuu komennolla git tag tagi1b HEAD^ , eli HEAD^ viittaa nykyistä "headia" eli olinpaikkaa historiassa edelliseen committiin
    • joissain windowseissa muoto HEAD^ ei toimi, sen sijasta voit käyttää muotoa HEAD~
    • tai katsomalla commitin tunniste (pitkä numerosarja) joko komennolla git log tai gitk:lla
  • kokeile molempia tapoja, tee niiden avulla kahteen edelliseen committiin tagit (tagi1a ja tagi1b)
  • katso komennolla gitk miltä historia näyttää

Tagit eivät mene automaattisesti etärepositorioihin. Pushaa koodisi githubiin siten, että myös tagit siirtyvät mukana. Katso ohje täältä

Varmista, etä tagit siirtyvät Githubiin:

kuva

7. Spring WebMVC

HUOM: tarvitset tässä tehtävässä mavenista version 3, ohjelma ei toimi versiolla 2. Kun asennat uuden version poista vanhan version koneellesi lataamat riippuvuudet poistamalla kotihakemistossasi oleva hakemisto .m2

Tarkastellaan edelliseltä viikolta tutun toiminnallisuuden tarjoamaa esimerkkiprojektia joka löytyy repositorion https://github.com/mluukkai/ohtu2016 hakemistossa viikko4/LoginWeb2

Hae projekti ja käynnistä se komennolla

mvn jetty:run

Jetty on keyvt HTTP-palvelin ja Servlettien ajoympäristö. Projektiin on konfiguroitu Jetty Maven-pluginiksi. Jos kaikki menee hyvin, on sovellus nyt käynnissä ja voit käyttää sitä web-selaimella osoitteesta http://localhost:8090 eli paikalliselta koneeltasi portista 8090.

Jos koneellasi on jo jotain muuta portissa 8090, voit konfiguroida sovelluksen käynnistymään johonkin muuhun porttiin esim. 9999:n seuraavasti:

mvn -D jetty.port=9999 jetty:run

SpringWebMVC:stä tällä kurssilla ei tarvitse ymmärtää. Kannattaa kuitenkin vilkaista tiedostoa ohtu.OhtuController.java, joka sisältää sovelluksen eri osoitteisiin tulevista kutsuista huolehtivan koodin. Kontrolleri käyttää AuthenticationService-luokkaa toteuttamaan kirjautumisen tarkastuksen ja uusien käyttäjien luomisen. Kontrolleri delegoi www-sivujen renderöinnin hakemiston WebPages/WEB-INF-views alla oleville jsp-tiedostoille.

Eli tutustu nyt sovelluksen rakenteeseen ja toiminnallisuuteen. Saat sammutettua sovelluksen painamalla konsolissa ctrl+c tai ctrl+d.

8. Selenium, eli web-selaimen simulointi ohjelmakoodista

Web-selaimen simulointi onnistuu mukavasti Selenium WebDriver -kirjaston avulla. Edellisessä tehtävässä olevassa projektissa on luokassa ohtu.Tester.java pääohjelma, jonka koodi on seuraava:

public static void main(String[] args) {
    WebDriver driver = new HtmlUnitDriver();

    driver.get("http://localhost:8090");
    System.out.println( driver.getPageSource() );
    WebElement element = driver.findElement(By.linkText("login"));
    element.click();

    System.out.println("==");

    System.out.println( driver.getPageSource() );
    element = driver.findElement(By.name("username"));
    element.sendKeys("pekka");
    element = driver.findElement(By.name("password"));
    element.sendKeys("akkep");
    element = driver.findElement(By.name("login"));
    element.submit();

    System.out.println("==");
    System.out.println( driver.getPageSource() );
}

Käynnistä sovellus edellisen tehtävän tapaan komentoriviltä. Varmista selaimella että sovellus on päällä.

Aja Tester.java:ssa oleva ohjelma. Esim. NetBeansilla tämä onnistuu valitsemalla tiedoston nimen kohdalta oikealla hiiren napilla "Run file".

Katso mitä ohjelma tulostaa.

Tester-ohjelmassa luodaan alussa selainta simuloiva olio WebDriver driver. Tämän jälkeen "mennään" selaimella osoitteeseen localhost:8090 ja tulostetaan sivun lähdekoodi. Tämän jälkeen haetaan sivulta elementti, jossa on linkkiteksti login eli

WebElement element = driver.findElement(By.linkText("login"));

Linkkielementtiä klikataan ja jälleen tulostetaan sivun lähdekoodi. Seuraavaksi etsitään sivulta elementti, jonka nimi on username, kyseessä on lomakkeen input-kenttä, ja ohjelma "kirjoittaa" kenttään komennolla sendKeys() nimen "pekka".

Tämän jälkeen täytetään vielä salasanakenttä ja painetaan lomakkeessa olevaa nappia. Lopuksi tulostetaan vielä sivun lähdekoodi.

Ohjelma siis simuloi selaimen käyttöskenaarion, jossa kirjaudutaan sovellukseen.

Jos koneessasi on Firefox, muuta Testerin pääohjelman rivi 11 muotoon:

WebDriver driver = new FirefoxDriver();

Suorita tester uudelleen. Jos koneesi Firefox-versio on yhteensopiva käytössä olevan selenium-version kanssa, sinun pitäisi nähdä Firefoxin käynnistyvän ja seleniumin "suorittavan" koodin skenaario Firefoxilla.

Muuta nyt koodia siten, että läpikäyt seuraavat skenaariot (jos FirefoxFriver ei toimi koneellasi, käytä alunperin koodissa käytettyä HtmlUnitDriver:ia):

  • epäonnistunut kirjautuminen: oikea käyttäjätunnus, väärä salasana
  • epäonnistunut kirjautuminen: ei-olemassaoleva käyttäjätunnus
  • uuden käyttäjätunnuksen luominen
  • uuden käyttäjätunnuksen luomisen jälkeen tapahtuva ulkoskirjautuminen sovelluksesta

HUOM: salasanan varmistuskentän (confirm password) nimi on passwordConfirmation

9. Web-sovelluksen testaaminen: easyB+Selenium

Pääsemme jälleen käyttämään viime viikolta tuttua easyB:tä. Hakemistosta Other Test Sources/easyb löytyy valmiina User storyn User can log in with valid username/password-combination määrittelevä story. Yksi skenaarioista on valmiiksi mäpätty koodiin. Täydennä kaksi muuta skenaariota.

Testit on konfiguroitu suoritettavaksi samalla tavalla kuin viime viikon easyB-tehtävässä. Huomaa, että voit testeissäkin halutessasi käyttää FirefoxDriver:ia.

Huom: Firefox ei jostain syystä toimi easyB:n kanssa jos testejä suoritetaan Travisissa. Jos joudut esim. miniprojektissa käyttämään FirefoxDriveria, joudut Travisin takia toteuttamaan testit jUnitilla.

10. Web-sovelluksen testaaminen osa 2

Kuten viime viikolta muistamme, toinen järjestelmän toimintaa määrittelevä User story on A new user account can be created if a proper unused username and a proper password are given

Löydät tämän Storyn easyB-pohjan viime viikon tehtävistä. Kopioi story projektiisi ja tee skenaarioista suoritettavia kirjoittamalla niihin Seleniumin avulla (edellisen tehtävän tyyliin) sovellusta testaavaa koodia. Muista lisätä story-tiedostoon Seleniumin vaatimat importit!

Huomioita

  • voit tehdä Tester.java:n tapaisen pääohjelman sisältävän luokan jos haluat/joudut debuggaamaan testiä. Toinen hyvä debuggaustapa on FirefoxDriverin käyttö
  • Uuden käyttäjän luomisen pohjalla käytettävään luokkaan UserData on määritelty validoinnit käyttäjätunnuksen muodon ja salasanan oikeellisuuden tarkastamiseksi. Eli toisin kuin viime viikolla, ei AuthenticationServicen tarvitse suorittaa validointeja.
  • Skenaarion "can login with succesfully generated account" mäppäävän koodin kirjoittaminen ei ole täysin suoraviivaista. Koska luotu käyttäjä kirjautuu automaattisesti järjestelmään, joudut kirjaamaan käyttäjän ensin ulos ja kokeilemaan tämän jälkeen että luotu käyttäjä pystyy kirjautumaan sivulle uudelleen.
  • Huomaa, että jos luot käyttäjän yhdessä testissä, et voi luoda toisessa testissä samannimistä käyttäjää uudelleen!

tehtävien kirjaaminen palautetuksi

tehtävien kirjaus:

  • Kirjaa tekemäsi tehtävät tänne
    • huom: tehtävien palautuksen deadline on su 17.4. klo 23.59

palaute tehtävistä:

  • Lisää viikon 1 tehtävässä 11 forkaamasi repositorion omalla nimelläsi olevaan hakemistoon tiedosto nimeltä viikko4
  • tee viime viikon tehtävän tapaan pull-request
    • anna tehtävistä palautetta avautuvaan lomakkeeseen
    • huom: jos teet tehtävät alkuviikosta, voi olla, että edellistä pull-requestiasi ei ole vielä ehditty hyväksyä ja et pääse vielä tekemään uutta requestia