Po 9ciu mc-ach od ostatniego wpisu mój pierwszy kurs wideo ujrzał światło dzienne.

.net_tests

Siemanko

Ostatni mój wpis na tym blogu miał miejsce dziewięć miesięcy temu. Z jednej strony to szmat czasu, z drugiej jednak te miesiące minęły bardzo szybko. Przez ten cały czas dużo pracowałem i przygotowywałem coś co od dawna miałem w swojej liście TODO powiązanej z mojej karierą w programowaniu. Dziś mogę tę pozycję oznaczyć jako wykonaną gdyż to właśnie dziś wraz z wydawnictwami VideoPoint oraz Helion wydałem swój pierwszy kurs wideo na temat testowania oprogramowania w trakcie developmentu w .Net Core. Traktuje to jako uzupełnienie mojego programistycznego CV tak samo jak mój GitHub.

Testowanie oprogramowania w .NET Core 2.0. Kurs video. Poziom pierwszy. Jak pisać dobry, niezawodny i łatwy w utrzymaniu kod

Mam nadzieje, że przypadnie Wam do gustu to w jaki sposób spróbowałem przekazać swoją wiedzę, dotychczasowe doświadczenie i spojrzenie na pisanie kodu i testów.

Jesli jesteście ciekawi jak wygląda praca nad stworzeniem kursu wideo lub chcielibyście również mieć taką publikację na swoim koncie – gorąco zachęcam! Jednak uprzedzam, że jest to znacznie trudniejsze niż się może wydawać na początku. Wymaga to dobrego planu, przygotowania i wytrwałości. Nie spisywałem ilości godzin, które poświęciłem na przygotowanie tego kursu jednak szacuję, że było ich ok 100 – więc wg mnie b. dużo.

Szczegółowy opis jak i spis treści znajdziecie na stronach kursu:

  • na portalu Helion – tutaj
  • na portalu ViedoPoint – tutaj

Jeśli się skusicie – dajcie feeback!

Pjona!

 

Selenium czyli jak zautomatyzować proces końcowego testowania aplikacji webowej.

Siemanko

W dzisiejszym poście pragnę nieco przybliżyć temat automatycznych testów end2end i pokazać przykładowy teścik.

Jak wszyscy pracujący m.in. przy webie wiemy, że ostateczne testy wykonują zazwyczaj ludzie klikając myszką i stukając w klawiaturę. Bez względu na fakt pokrycia kodu testami jednostkowymi, które przed release’em mienią się na zielono. Jeśli Twoim lub Twojej firmy mottem nie jest hasło „Testujemy na produkcji!” to na pewno Wasz dział testerski (o ile go macie) lub Wy sami musicie klikać jak małpka te same user stories i interpretować co się dzieje. Jeśli release’y są np. co pół roku to spoko – duże wydarzenie więc wszystkie ręce na pokład testerskiego okrętu. Jednak jeśli release’y są co np. tydzień (hot fixy itp.) to aby być pewnym, że zmiany w nadchodzącym release’ie nie popsuły niczego w aktualnym trzeba wszystko od początku do końca przeklikać co przy zaawansowanych aplikacjach może zająć b. długo. O kosztach takiego postępowania (pieniężnych i emocjonalnych) chyba nie muszę pisać. Czy jest coś co możemy z tym zrobić? Owszem, jest.

User story w teście jednostkowym

Selenium umożliwia przetestowanie działania aplikacji internetowej z poziomu testu jednostkowego. Udostępnia ono „drivery” do większości przeglądarek inetrnetowych i tak jak „Karma” (jeśli pracujesz przy froncie to wiesz o co kaman) otwiera okno przeglądarki i klika, wpisuje, przesuwa, robi dokładnie to co zakodujesz w teście, a że jest SDK dla .NETu to jesze wynik wyświetli Ci się na czerwono lub zielono w Test Explorer’ze w VS.

Test case

Na pokaz zróbmy taki test SEO w google. Chcemy sprawdzić czy po wpisaniu „Selenium” w google główna strona selenium będzie pierwszym wynikiem.

test1

Klikamy Run Test. Selenium otworzy przeglądarkę (w tym przypadku Chrome), wpisze „Selenium” i kliknie szukaj. Następnie pobierze adres strony pierwszego wyniku i zostanie on przyrównany do oczekiwanego.

ezgif-3-2306a82b10

Ok, poszło. Ale my jesteśmy programistami i nie mamy takich pomysłów co można dziwnego zrobić z aplikacją jak testerzy. Wspomniani natomiast patrząc na ciąg tych znaczków w XPath też mogą nie kumać czaczy i Oni kolejnych testów nie napiszą. Dlatego w tym momencie do akcji wchodzi POM (nie, nie powolny objazd miasta) – Page Object Model.

Page Object Model

bumblebee_logo

To pattern umożliwiający testerom nie posiadającym wiedzy programistycznej na skuteczne i bezproblemowe pisanie testów w Selenium. Ten pattern można stosować dzięki BumbleBee. Udostępnia ona wiele implementacji tagów html’owych, które posiadają user, a raczej tester friendly API.

POM dla głownej strony wyszukiwania.

page1

POM dla strony z wynikami wyszukiwania

page2

Za POM odpowiedzialny jest programista i ma w nim dostarczyć testerowi dokładnie to co user może kliknąć na stronie. W naszym uproszczonym przypadku wyłuskałem przy użyciu BumbleBee tylko pole wyszukiwania, submit button oraz pierwszą stronę wyników. Teraz tester, gdy ma dostarczony taki POM, po krótkim przeszkoleniu może pisać test case’y jako testy jednostkowe bo pole wyszukiwania ma metodę AppendText(), a button ma metodę Submit(). Trzeba tylko znać angielski aby sie połapać.

test2

Podsumowanie

To oczywiście tylko drobna zajawka, gdyż sam całkiem niedawno dopiero się dowiedziałem o istnieniu tego cuda. Po więcej odsyłam do Selenium i BumbleBee. Kodzik stworzony na potrzeby posta jak zwykle na moim github’ie

Pjona!

Jak zmapować JObject do konkretnej klasy za pomocą AutoMappera.

Siemanko

Wykorzystując generyczne możliwości C# oraz bibliotekę AutoMapper do mapowania modeli na podstawie konwencji, można zaimplementować np. operacje CRUDowe dla wielu modeli bez potrzeby powtarzania kodu konkretnej implementacji dla konkretnego modelu. Ostatnio, implementując coś w rodzaju kolejki eventów oraz ich obsługi, musiałem się na chwilę zatrzymać przy mapowaniu za pomocą AutoMappera zdeserializowanego (Newtonsoft.Json) obiektu do obiektu konkretnej klasy.

Zwykłe CreateMap<> nie działa.

Na początku stworzyłem najprostszy profil mapowania.

    public class SomeClassProfile : Profile {
        public SomeClassProfile() {
            CreateMap<JObject, SomeClass>();
        }
    }

To jednak nie zadziała i efektem mapowania będzie obiekt z default’owymi wartościami propert.

notworking

Pewnie się zastanawiasz czemu od razu nie deserializuje do konkretnej klasy – otóż w realnym problemie, który rozwiązywałem przy deserializacji jeszcze nie wiedziałem jakiego typu jest to obiekt.

Trochę poguglałem i okazało się, że trzeba swtorzyć niecio bardziej osobliwy mapping z wykorzystaniem JsonSerializer

    public class SomeClassProfile : Profile {
        public SomeClassProfile() {
            CreateMap<JObject, SomeClass>().ConstructUsing((jObject) => {
                var someClassObject = new SomeClass();

                var serializer = new JsonSerializer();
                serializer.Populate(jObject.CreateReader(), someClassObject);

                return someClassObject;
            });
        }
    }

Teraz wszystko bangla jak należy.

working

Wrzucam to tutaj głównie z myślą o sobie, abym w przyszłości mógł zrobić szybkiego copy-paste’a jeśli mi się zapomni, ale nie wykluczone, że przyda się to także i Tobie.

Pjona!

RabbitMQ, .NET Core, Nancy Fx, MongoDb – przykład kolejkowania zdarzeń.

Siemanko.

Jak już wspominałem we wcześniejszych postach od pewnego czasu staram się zgłebiać wiedzę na temat systemów rozproszonych i podejścia DDD. Im głebiej w las tym bardziej się jaram i zarazem dostrzegam ułomności standardowego monolitycznego podejścia do budowania aplikacji z pseudo warstwami abstrakcji, które i tak w końcu zamieniają się w spaghetti code (niestety). W tym poście opiszę przykład asynchronicznej komunikacji między aplikacjami poprzez serwer RabbitMQ.

Założenia

Dwie osobne aplikacje .NET Core z wykorzystaniem Nancy FX (bo tak, ale może być Mvc). Pierwsza aplikacja po wejściu na „index” publikuje zdarzenie do kolejki RabbitMQ z DateTime.Now jako danymi (może być cokolowiek).

Druga aplikacja po uruchomieniu „rejestruje” się w kolejce RabbitMQ jako subskrybent i zapisuje każde dane z odczytanego zdarzenia (czyli DateTime.Now w moemencie publikacji). Po wejściu na jej „index” wyświetla listę wszystkich DateTime.Now z odczytanych zdarzeń.

Crew propgramu to to, że druga aplikacja wcale nie musi być uruchomiona aby pierwsza mogła działać i robić swoje (czyli w tym przypadku tylko publikować zdarzenia na każde wejście na „index”) i żeby nic nie zostało utracone.  W momencie kiedy druga aplikacja wystartuje odczyta wszystkie wiadomości z kolejki i zapisze je w swojej bazie danych.

rabbit1

Instalacja

  1. Pobrać i zainstalować Erlang (potrzebne do rabita) – http://www.erlang.org/downloads
  2. Pobrac i zainstalować RabbitMQ – https://www.rabbitmq.com/download.html
  3. Po instalacji Rabbita upewnić się czy masz ustawione wszystkie zmienne – https://www.rabbitmq.com/install-windows-manual.html
  4. Odpalić w konsoli – rabbitmq-plugins enable rabbitmq_management
  5. Odpalić w przeglądarce – http://localhost:15672
  6. Zalogować się – login: guest, hasło: guest

Jesli wszystko poszło dobrze to powinieneś zobaczyć panel zarządzania

2

Producent

To co musi zrobić producent to podłączyć się do kolejki (zostanie utworzona jeśli jej nie ma) i opublikować zdarzenie:

private readonly IModel channel;
private readonly IConnection connection;
private readonly string queueName;

public EventSender()
{
   queueName = "helloWorldQueue";
   var factory = new ConnectionFactory() { HostName = "localhost" };
   this.connection = factory.CreateConnection();
   this.channel = connection.CreateModel();
   this.channel.QueueDeclare(queue: queueName,
   durable: false,
   exclusive: false,
   autoDelete: false,
   arguments: null);
}

public void SendEvent(string message)
{
   var body = Encoding.UTF8.GetBytes(message);

   this.channel.BasicPublish(exchange: "",
                             routingKey: queueName,
                             basicProperties: null,
                             body: body);
}

Po uruchomieniu i wejściu na „index”

3

wystarczy wywołać seriwisik publikujący zdarzenie. Po klikukrotnym odświeżeniu zajrzyjmy do rabbita:

4

Jest 8 wiadomości w kolejce, a aplikacja obsługująca te wiadomości nie jest jeszcze uruchomiona – ba! nawet jeszcze jej nie ma ;).

Subskrybent

Aplikacja przy starcie rejestruje się w kolejce

private readonly IMessagesService messagesService;
private IModel channel;
private IConnection connection;
private readonly string queueName;
private EventingBasicConsumer consumer;

		public EventConsumer(IMessagesService messagesService)
		{
			this.messagesService = messagesService;
			this.queueName = "helloWorldQueue";
		}

		public void Start()
		{
			var factory = new ConnectionFactory() { HostName = "localhost" };
			this.connection = factory.CreateConnection();
			this.channel = connection.CreateModel();
			this.channel.QueueDeclare(queue: queueName,
				durable: false,
				exclusive: false,
				autoDelete: false,
				arguments: null);

			this.consumer = new EventingBasicConsumer(this.channel);
			consumer.Received += (model, ea) =>
			{
				var body = ea.Body;
				var message = Encoding.UTF8.GetString(body);

				messagesService.InsertMessage(message);
			};
			channel.BasicConsume(queue: this.queueName,
				noAck: true,
				consumer: consumer);
		}

Niech obiekt tej klasy będzie singletonem aby był ciągle podłączony do kolejki i mógł odczytywać wiadomości na bieżąco. Imlementacja IMessagesService to warstwa dostępu do danych. Każdą odczytaną wiadomość zapisuje do bazy (w tym przypadku użyłem Mongo). Po wejściu na „index” subskrybenta wszystkie zapisane wiadomości zostaną wyświetlone:

6

Podsumowanie

Cały kodzik obu aplikacji dostępny tutaj.

Pjona!

Azure Function czyli po co Ci serwer?

Siemanko

Ostatnio (dopiero?) podczas zgłębiania wiedzy o mikroserwisach usłyszałem o serverless – czyli architekturze webowej, która nie wymaga klasycznego serwera. Opiera się ona na wywoływaniu funkcji będących w chmurze. Oczywiście my te funkcje musimy napisać i wrzucić do chmury. Obsługę funkcji wg mojej wiedzy oferuje Amazon, Azure i Google Cloud. Jako entuzjasta Microsoft’u założyłem konto na Azure z darmowymi 170cioma euro do wykorzystania przez pierwsze 30 dni. W kilka minut (łącznie z rejestracją) napisałem prostą funckje, która zwraca htmla (wszystko w aplikacji Azure’a w przeglądarce).

using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");
    await Task.FromResult(true);

    var pageTemplate = "{0}"; //html document template in string which can't be pasted here
    var pageBody = "Hello World from Azure Function which was made in less time than your coffe!";

    var response = new HttpResponseMessage();
    response.Content = new StringContent(String.Format(pageTemplate, pageBody));
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");

    return response;
}

Wraz z kliknięciem „Uruchom”

uruchom

Funkcję już można odpalać pod tym adresem.

Dla mnie bomba!

Więcej na stronie Azure’a.

Pjona!

Nancy – czemu my się dopiero poznaliśmy?

Siemanko

Ostatnio zakupiłem polecaną przez kilka znanych osobistości w polskim community książkę „Microservices in .NET Core” gdyż temat mnie interesuje i postanowiłem przyjrzeć sie temu bliżej. We wstępie autor informuje iż w stacku technologicznym wykorzystywanym w przykladach w książce góruje Nancy, której autor jest współtwórcą. Czym jest Nancy mniej więcej wiedziałem bo dość często przewija się to słowo w artykułach, które zdarza mi się czasem przeczytać 😉 Jednak to „mniej więcej” oznaczało jedynie tyle, że wiedziałem o istnieniu tego czegoś ale niekoniecznie co to coś robi ;). Z racji zamiaru przyswojenia całej wiedzy z w.w. książki postanowiłem dziś przyjrzeć się Nancy i po raz pierwszy spróbowac użyć.

Bajecznie prosta i lekka aplikacja webowa

public class IndexModule : NancyModule 
    {
        public IndexModule() 
        {
            Get["/"] = parameters => 
            {
                return View["index"];
            };
        }
    }

Powiedzmy, że to jest odpowiednik kontrolera w Asp.Net.

Teraz w klasie „Program.cs” wystartujmy naszą aplikacje webową

static void Main(string[] args) 
       {
            var uri = new Uri("http://localhost:8500");

            var config = new HostConfiguration();
            config.UrlReservations.CreateAutomatically = true;

            using (var host = new NancyHost(config, uri)) 
            {
                host.Start();

                Console.ReadLine();
            }
        }

Teraz po uderzeniu pod adres http://localhost:8500 (u mnie) dostaniemy stronkę z htmlem z pliczku „index.sshtml”. To wszystko!

Proste? Proste! I jakie lekkie!

nancy

Zużycie pamięci działającej aplikacji Nancy

Zrobiłem dla porównania na szybko aplikacje Asp.Net Core z jednym kontrolerem i widokiem (z template’u z Visual Studio). Różnica w zasobach jest znacząca.

asp

Zużycie pamięci aplikacji Asp.Net Core z jednym kontrolerem i widokiem

 

[EDIT Start 24.03.2017]

W komentarzach zarzuca mi się, że porównuje dwie wersje frameworka. Oczywiście nie miałem tego na celu tylko chciałem pokazać  jaka jest różnica w zasobach między aplikacją webową zrobioną „na szybko” z temaplate’u z VS (i tu poprostu padło na .core), a aplikacją „na szybko”, która można zrobić z Nancy.

W celu sprostowania zrobiłem pustą aplikację Asp.Net Core z dodanym Mvc oraz drugą pustą aplikację z dodanym Owin’em i Nancy. Aplikacje zwracają tylko jeden widok „Hello Wolrd”. Różnica w zasobach jest rzędu 50% na korzyść aplikacji z wykorzystaniem Nancy. Zajmę się tym konkretniej i opiszę to w kolejnym poście.

[Edit End]

 

Zakochałem się! 😉

Pjona!

[Subiektywnie] Dobra pozycja do zrozumienia czym jest DDD i jakie niesie ze sobą korzyści.

Simanko.

Od długiego czasu słyszy się o DDD. Sam słuchałem w 2015r. jednej prelekcji na żywo natomiast drugą oglądałem na prulal sight (Dino Esposito). Godzinna czy dwu godzinna pogadanka w moim przypadku zbyt wiele mi nie uświadomiła prócz tego, że DDD to nie jedynie technologie wykorzystywane do tworzeniu kodu, a filozofia, podejście do całego procesu wytwarzania oprogramowania, które nie dotyczy tylko zespołu programistów, a całego przedsiębiorstwa. Jeśli masz podobnie, a jednak ciekawi Cię o co kaman w tym całym Domain Driven Design to polecam Ci książkę DDD autorstwa Vaughn Vernon’a. Sam kupiłem ją przypadkiem, ponieważ dowiedziałem się pewnej soboty, że akurat tego dnia była przeceniona o 50% w helionie więc grzechem było nie skorzystać z takiej okazji :). Czytam ją w wolnym czasie i w tej chwili mam za soba ok 1/3 książki, a już dostrzegłem jak różne jest DDD od podejścia np. u mnie w firmie (w której pracuję) i jak bardzo wiele popełniliśmy (nadal popełniamy) błędów.

Na tę chwilę jestem najbardziej zajarany architekturą sterowaną zdarzeniami i magazynowaniem zdarzeń (event source’ingiem) dlatego możecie się niedługo spodziewać nowych kodów na moim github’ie z jakimiś małymi wymyślonymi aplikacjami, w których będę starał się implementować wzorce wykorzystywane w DDD.

Pjona!

Prosty generator memów.

Siemanko.

Jak zapowiadałem w poprzednim poście będę strał się wrzucać na github’a każdy kodzik, który napiszę, a tutaj będę opisywał powód jego powstania.

Spory portal na horyzoncie

Dostałem nie dawno  do wyceny draft portalu internetowego, który ma zamiar powstać. Jest w nim na prawdę dużo funkcjonalności, które trzeba spiąć w portal rankingowy. Na długiej liście funkcjonalności jest sporo takich, których nigdy nie robiłem więc ciężko mi się określić ile taki task może zająć. Takim taskiem jest m.in. generator memów. Korzystając z wolnego popołudnia spróbowałem coś zrobić i jak się okazało nie jest to takie trudne 😉 Oczywiście w najprostszej formie, ale dzięki temu wiem, że nie będę ślęczał nad tym wiele dni, a raczej zamknie się to w kilkunastu godzinach.

Jak?

Z użyciem elementu canvas i javascriptu. Nie ma co tu za dużo gadać. Wszystko jest na githubie.

gif-generator

Pjona!

Workaround umożliwiający czytanie widoków SQL w Entity Framework Core 1.1.0

Siemanko.

Od dawna nic nie piszę, ale postanowiłem to zmienić i co więcej, oprócz pisania planuje troszkę publicznie pokodować i pushować na github’a.

Często rozmyślam nad problemami pojawiającymi się podczas codziennej pracy z różnymi technologiami i równie często tworze w domu nowe solucje w Visual Studio tylko po to aby sprawdzić pomysły na rozwiązania wcześniej wspomnianych problemów. Ostatnio zadałem sobie pytanie czemu by tego nie wrzucać do sieci i w dodatku mieć jakiś temat na post na tym moim nieszczęsnym blogu? 😉 Dlatego od teraz każdy poryty kod bedzie lądował na githubie, a tu z kolei kilka zdań na temat przyczyn jego powstania 🙂

EF Core nie wspiera widoków SQL

Ostatnio w robocie piszę funkcjonalności związane z raportowaniem ilościowym danych wg wielu kryteriów. W projekcie używamy tylko i wyłącznie Microsoft’owego ORMa w wersji Core (gdyż aplikacja jest w Asp.Net Core). Przy tego typo raportach, które mam okazję implementować EF wypada bardzo słabo czasowo ponieważ where’owanie czy group’owanie po kolumnie w czwartej z kolei join’owanej tabeli powoduje, że ef musi wykonać całe query jeszcze przed uwzględnieniem where’a aby mieć zmapowane dane z tej czwartej join’owanej tabelki żeby w końcy wykonać na tych danych where’a. 🙂 Wiem, nieźle zakręciłem, ale chodzi finalnie o to że mimo iż w wyniku dostaniesz 10 rekordów to EF może wcześniej wykonać query na 10000 tys. rekordów żeby mieć dostęp do danych z którejś z kolei zjoinowanej tabelki, które zostały uwzględnione w klauzuli where :). W celu poprawy wydajności pomyślałem żeby stworzyć widok SQL, w którym będę miał wszystkie potrzebne kolumny. Niestety EF Core nie wspiera (jeszcze) widoków w przeciwieństwie do EF 6.

Workaround

Wymyśliłem sobie kontekst sklepu. W sklepie jest klilka produktów, które sa pogrupowane w kategorich. Produkt może byc w wielu kategoriach i kategoria może mieć wiele produktów więc mamy tabelkę łączącą relacje wiele do wielu. Oprócz tego produkty sa powiązane relacją jeden do wielu z tabelka zdjęć. Zdjęcie może być tylko dla jednego produktu, a produkt może mieć wiele zdjęć. Chcę pobrać listę kategorii z ich podstawowymi informacjami wraz z jednym losowym produktem i jego zdjęciem oznaczonym jako IsMinPhoto (czyli miniaturka).

Ef Core umożliwia wykonania selecta z czystego SQL poprzez metodkę FromSql(). Metodka ta może zostać wywołana na propercie DbSet<> obiektu DbContext. Utworzyłem sobie zatem model zawierający kolumny, które będą w widoku. Po utworzeniu bazy zostanie utworzona taka tabela jednakże będzie ona zawsze pusta. Tabela wymaga posiadania klucza Id na podstawie, którego EF mapuje rekordy z bazy danych na obiekty jednakże widok nie posiada swojego Id, gdyż jest to tylko zestaw kolumn z innych tabel. Jako klucz dla modelu odwzorującego widok dodałem pole typu string. Bardzo ważne jest aby odpowiednik tego pola znalazł sie także w widoku z tabelki, której rekordy się nie powtarzają w żadnej relacji (w moim przypadku tabelka Photo). Następnie utworzyłem widok SQL w SQL Managment Studio z użyciem designera.

model

Model EF

Bez tytułu.jpg

Widok SQL w SQL Mangment Studio

Czytanie z widoku

queryview

Podsumowanie

Zrobiłem porównanie czasowe z normalnym zapytaniem zrobionym przez Linq na ORMie i przy łącznie raptem kilkunastu rekordach we wszystkich tabelach wyciąganie danych z widoku jest 3krotnie szybsze i w Sql Profilerze widać tylko jedno zapytanie natomiast gdy robił to ORM zapytań w tym przypadku było 3.

Kodzik z testami do uruchomienia znajduje się na githubie.

Pjona!

Asp.Net Core 1.0 – camel case domyślnym formatowaniem Jsona w Mvc.

Siemanko.

W wersjach RC (Rc1 i Rc2) przy zwracaniu danych z API (mvc) zserializowanych do jsona za pomocą metody:

Json()

obiekty były (defaultowo) formatowane bez zmian wielkości liter w nazwach

2

4

W wersji RTM defaultowym ustawieniem formatowania jest camel case:

2

1.png

Jeśli rozwijacie projekt od wersji RC i planujecie go przenieść na wersję RTM to może Wam to zajebiście namieszać 🙂 Jednak niech będą spokojne Wasze rozczochrane – wujcio ma rozwiązanie ;). W klasie Sturtup.cs w metodzie ConfigureServices, w miejscu gdzie macie dodane Mvc należy troszkę dopisać do tej linijki:

3

Od teraz będzie wszystko po staremu:

2

4

Pjona!