Spis treści
W tym rozdziale:
- dowiemy się więcej o fazach budowy projektów Mavenowych,
- zobaczymy jak konfigurować pluginy i korzystać z ich zadań,
- użyjemy pluginu Assembly do wygenerowania pliku JAR zawierającego wszystkie zależności,
- nauczymy się jak konfigurować plugin do wykonywania testów,
- dodamy do projektu możliwość uruchamiania testów integracyjnych.
Fazy budowy projektu i pluginy¶
Maven buduje nasze projekty poprzez wykonanie szeregu zależnych od siebie faz (build lifecycle phases). Każda faza odpowiedzialna jest za wykonanie określonego zadania.
Kilka z tych fazy widzieliśmy w poprzednim rozdziale – były to m. in. fazy:
- compile, podczas której źródła projektu są kompilowane,
- test, w której wykonywane są testy,
- package, której wynikiem jest np. plik JAR lub WAR,
- install, dzięki której plik wygenerowany w fazie package jest przenoszony do lokalnego repozytorium .m2, i staje się dostępny z poziomu innych naszych projektów.
Ponieważ fazy są od siebie zależne, uruchomienie np. fazy test w Maven za pomocą komendy:
powoduje wykonanie przez Maven wszystkich poprzednich w kolejności faz. Jeżeli zakończą się one sukcesem, na końcu zostanie wykonana zlecona przez nas faza test.
Listę wszystkich faz możemy otrzymać korzystając z pluginu Help Mavena:
Każda faza zależna jest od faz poprzednich, więc aby wykonać fazę deploy Maven musi najpierw przejść przez ponad 20 wcześniejszych faz.
Jak widać na powyższym listingu, niektóre z faz mają przyporządkowane informacje o pluginach. Dla przykładu, do fazy test przypisany jest plugin Surefire:
org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
Pierwsze trzy człony informacji o tym pluginie to poznane już groupId, artifactId, oraz version – pluginy to także projekty Mavenowe. Na samym końcu opisu tego pluginu znajduje się zadanie tego pluginu (plugin goal), które ma zostać wykonane przez Maven w trakcie fazy test. W tym przypadku zadanie pluginu, jak i faza budowania projektu, z którą to zadanie jest skojarzone, mają taką samą nazwę – test.
To, jakie zadania udostępniają pluginy, zależy od twórców tych pluginów. Dla przykładu, do tej pory korzystaliśmy z zadań describe (w tym rozdziale) oraz evaluate (w poprzednim rozdziale) pluginu Help Mavena:
Listę zadań danego pluginu możemy sprawdzić zaglądając do oficjalnej dokumentacji. Innym sposobem, aby zapoznać się z możliwościami pewnego pluginu, jest użycie pluginu Help Mavena.
Plugin Help ma zadanie describe, któremu możemy przekazywać różne parametry. Jednym z nich jest plugin. Jeżeli z niego skorzystamy, Help zwróci informację o podanym przez nas pluginie:
Powyżej kazaliśmy pluginowi help wykonać zadanie describe (help:describe), przekazując parametr plugin o wartości org.apache.maven.plugins:maven-surefire-plugin
W zwróconych informacjach widzimy szczegółowe dane pluginu, o który zapytaliśmy, czyli pluginu Surefire. Widzimy, że udostępnia on ma dwa zadania: test oraz help.
Generowanie pliku JAR z zależnościami¶
Nasze projekty zazwyczaj będą miały wiele zależności do innych projektów. Aby uruchomić nasz program będziemy musieli ustawić w classpath ścieżki do wymaganych JARów. Możemy zamiast tego tak skonfigurować Maven, aby był w stanie wygenerować jeden duży JAR, który będzie zawierał klasy naszego projektu oraz wszystkie jego zależności. W tym celu skorzystamy z pluginu Maven o nazwie Assembly.
W tym rozdziale utworzymy nowy projekt za pomocą generatora archetypów Maven – ponownie będzie to archetyp maven-archetype-quickstart. Użyjemy trybu nieinteraktywnego, przekazując wszystkie potrzebne wartości jako argumenty:
Po wygenerowaniu projektu dodamy do pliku pom.xml zależność do Log4j, aby później przetestować, czy plik JAR wygenerowany przez plugin Assembly będzie zawierał zależność do tej biblioteki. Poniższe wpisy dodajemy do elementu <dependencies>:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.13.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.13.1</version> </dependency>
Wygenerowaną w katalogu src/main/java/com/kursjava/maven klasę App.java zastąpimy plikiem FactorialCounter.java o następującej treści:
package com.kursjava.maven; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class FactorialCounter { private static final Logger log = LogManager.getLogger(FactorialCounter.class); public static void main(String[] args) { System.out.println("Przyklad liczenia silni."); System.out.println("Silnia 5 = " + factorial(5)); } public static int factorial(int n) { if (n < 0) { log.error("Nieprawidlowa wartosc n: {}", n); throw new IllegalArgumentException( "Silnia moze byc liczona tylko dla n >= 0" ); } int result = 1; for (int i = 2; i <= n; i++) { result *= i; } return result; } }
Ta prosta klasa zawiera metodę liczącą silnię podanej liczby. Korzysta ona z Log4j do ewentualnego zalogowania błędnego argumentu.
Do projektu dodamy jeszcze jeden zasób – konfigurację Log4j. Nie musimy tego robić, ponieważ Log4j mógłby użyć swojej domyślnej konfiguracji, ale jest to dobry przykład, by zobaczyć gdzie w projektach Mavenowych umieszcza się tego rodzaju zasoby.
Poniższy plik log4j2.xml umieszczamy w katalogu src/main/resources projektu:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss} %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>
Jeżeli zbudujemy teraz nasz projekt i spróbujemy uruchomić go korzystając ze standardowo wygenerowanego pliku JAR w katalogu target, to zobaczymy następujący komunikat błędu:
Powodem komunikatu jest brak biblioteki Log4j w classpath, od której zależna jest nasza klasa z pliku FactorialCounter.java.
Moglibyśmy JAR z Log4j umieścić w classpath, ale zamiast tego skorzystamy z pluginu Assembly do wygenerowania JARa zawierającego naszą klasę wraz ze wszystkimi zależnościami.
Zanim będziemy mogli skorzystać z pluginu Assembly, musimy skonfigurować go w pliku pom.xml projektu. Konfigurację pluginów umieszcza się w elementach <plugin>, dla których elementami nadrzędnymi są elementy <plugins> i <build>.
Aby móc korzystać z pluginu Assembly, wystarczy dodać poniższy element do pliku pom.xml:
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.2.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>
Możemy teraz skorzystać z zadania single pluginu Assembly, którego celem jest wygenerowanie jednego JARa zawierającego klasy naszego projektu i ich zależności. W pierwszej kolejności musimy skompilować nasz projekt. Możemy połączyć obie komendy rozdzialając je spacją:
W wyniku działania pluginu Assembly wygenerowany został następujący plik w katalogu target: policz-silnie-1.0-SNAPSHOT-jar-with-dependencies.jar
Ten plik zawiera zarówno skompilowaną klasę naszego projektu, jak i wszystkie zależności wymagane w trakcie jego działania – w naszym przykładzie jest to biblioteka Log4j.
Jeżeli otworzymy ten plik JAR, to zobaczymy następujace pliki i katalogi:
W katalogu com znajduje się skompilowana klasa naszego projektu, a wszystkie pozostałe katalogi i pliki należą do Log4j.
Możemy ponownie spróbować uruchomić główną klasę naszego projektu, tym razem korzystając jednak z nowego pliku JAR:
Tym razem udało nam się uruchomić naszą klasę. Wszystkie zależności naszego projektu (aż jedna – Log4j) zawarte są w pliku JAR policz-silnie-1.0-SNAPSHOT-jar-with-dependencies.jar wygenerowanym przez plugin Assembly.
Korzystanie ze zmiennych w pliku pom.xml¶
W plikach pom.xml możemy definiować nazwane wartości, z których możemy potem korzystać w pliku pom.xml. Dla przykładu, w poprzednim rozdziale dodaliśmy zależność do biblioteki Log4j – wymagało to użycia dwóch następujących wpisów:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.13.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.13.1</version> </dependency>
Wersja obu zależności jest taka sama – zamiast wpisywać ją na sztywno w obu elementach <version>, możemy utworzyć property z tą wartością i użyć jej w elementach <dependency>.
W plik pom.xml wygenerowanego projektu jest już kilka nazwanych wartości:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties>
Dodamy do nich nową wartość o nazwie log4j.version:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> <log4j.version>2.13.1</log4j.version> </properties>
Aby wskazać Mavenowi, że ma skorzystać z pewnej nazwanej wartości, stosujemy składnię ${nazwa} (znak dolara, klamra, nazwa, klamra), co widać na poniższym listingu:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j.version}</version> </dependency>
Dzięki zastąpieniu wpisanych na sztywno wersji tych zależności możemy w łatwy sposób zmienić wymaganą wersję zmieniając ją tylko w jednym miejscu naszego pliku pom.xml – w elemencie <log4j.version> zdefiniowanym w <properties>.
Podłączanie zadania pluginu do fazy budowy projektu¶
Na początku rozdziału o pluginach zobaczyliśmy, że budowa projektu skłąda się z wielu faz, ale tylko niektóre z nich mają domyślnie przypisane zadanie pewnego pluginu, które ma zostać przez ten plugin wykonane.
Dla przypomnienia, spójrzmy na fragment listy faz budowy projektu i przypisanych do nich zadań pluginów:
Faza verify nie ma powiązanego zadania żadnego pluginu, a podczas fazy package wykonywane jest zadanie jar pluginu maven-jar-plugin.
W pliku pom.xml możemy skonfigurować pluginy w taki sposób, by jedno z ich dostępnych zadań zostało wykonane automatycznie, gdy Maven będzie wykonywał pewną fazę budowy projektu.
Wcześniej w tym rozdziale skorzystaliśmy z pluginu Assembly do zbudowania jednego, dużego pliku JAR naszego projektu, zawierającego wszystkie zależności. Możemy dodać do naszego pliku pom.xml konfigurację, dzięki której plugin Assembly będzie wykonywał swoje zadanie zawsze podczas np. fazy package. Dzięki temu zawsze budując nasz projekt będziemy dodatkowo otrzymywali w katalogu target plik JAR z zależnościami, bez potrzeby ręcznego wywoływania pluginu Assembly z lini poleceń.
Aby dodać użycie Assembly do fazy package, uzupełniamy konfigurację tego pluginu o element <executions>, w którym definiujemy fazę, podczas której plugin ma zostać użyty, oraz które z jego zadań ma wtedy zostać wykonane. Konfiguracja pluginu Assembly będzie więc w naszym projekcie wyglądać następująco:
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.2.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
W elemencie <phase> podajemy nazwę fazy, podczas której plugin ma zostać użyty, a w elemencie <goal> – zadanie, które plugin ma wtedy wykonać.
Jeżeli zbudujemy teraz od nowa projekt, to zobaczymy, że w katalogu target, poza plikiem JAR standardowo generowanym w fazie package, znajduje się także drugi plik JAR – ten wygenerowany przez podłączony przez nas do fazy package plugin Assembly:
Maven w fazie package poza standardowym wykonaniem zadania jar pluginu maven-jar-plugin, wykonał także zadanie single pluginu maven-assembly-plugin, co zostało zaznaczone na powyższym listingu.
Konfiguracja i uruchamianie testów jednostkowych¶
W fazie test, Maven domyślnie wykonuje testy zawarte w plikach projektu, których nazwa pasuje do któregoś z poniższych wzorców (** oznacza dowolny katalog projektu):
- **/Test*.java
- **/*Test.java
- **/*Tests.java
- **/*TestCase.java
Testy powinniśmy umieszczać w katalogu src/test/java. Jeżeli mamy potrzebę korzystać z pliku z testami, który nie pasuje po powyższych wzorców, to możemy skonfigurować plugin Surfire, którego Maven używa do uruchamiania testów, aby brał pod uwagę pliki z testami o innych nazwach.
Dla przykładu, dodajmy do katalogu src/test/java plik o nazwie CheckFactorial.java, w którym dodamy dwa testy jednostkowe klasy FactorialCounter, którą dodaliśmy do projektu w jednym z poprzednich rozdziałów:
package com.kursjava.maven; import static org.junit.Assert.assertEquals; import org.junit.Test; public class CheckFactorial { @Test public void shouldReturnFactorial() { assertEquals(120, FactorialCounter.factorial(5)); } @Test(expected = IllegalArgumentException.class) public void shouldThrowExceptionForInvalidArgument() { FactorialCounter.factorial(-1); } }
Jeżeli teraz zlecimy Mavenowi wykonanie testów, to testy z naszego pliku CheckFactorial nie zostaną wykonane. Jedyny test, jaki się wykona, to ten zawarty w pliku AppTest.java, który został utworzony podczas generowania projektu za pomocą generatora archetypów:
Aby plugin Surefire brał pod uwagę testy zawarte w naszym pliku CheckFactorial.java, musimy skonfigurować go w pliku pom.xml:
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> <configuration> <includes> <include>**/*Test.java</include> <include>CheckFactorial.java</include> </includes> </configuration> </plugin>
Pliki z testami, które ma brać pod uwagę plugin Surefire, umieszczamy w elementach <include>. Zauważ, że dodaliśmy także <include> z **/*Test.java, ponieważ ręczna konfiguracja nazw plików testowych powoduje, że domyślne wzorce, które przedstawiłem na początku rozdziału, przestają być używane. Gdybym pominął ten <include>, to jedynie testy z pliku CheckFactorial.java byłyby wykonywane.
Jeżeli teraz wykonamy testy, to zobaczymy, że wykonane zostały testy z obu plików:
Czasami możemy mieć potrzebę wykonać testy jednostkowe z jednego, konkretnego pliku. Aby wskazać ten plik, korzystamy z parametru test, którego wartością powinna być nazwa klasy (bez rozszerzenia):
Za pomocą mvn -Dtest=CheckFactorial test zleciliśmy Mavenowi wykonanie testów w klasie CheckFactorial – nazwę tej klasy ustawiliśmy jako parametr o nazwie test (parametr określający plik z testami do wykonania i faza testów nazywają się w tym przypadku tak samo – test).
Dodawanie testów integracyjnych do fazy verify¶
Poza testami jednostkowymi, Maven może także uruchamiać dla nas testy integracyjne w fazie verify.
Domyślnie jednak funkcjonalność ta nie jest włączona. Aby Maven wykonał testy integracyjne, musimy do pliku pom.xml dodać konfigurację pluginu o nazwie Failsafe, którego zadaniem jest właśnie uruchamianie testów integracyjnych.
Plugin Failsafe domyślnie wykonuje testy w plikach, które pasują do któregoś z poniższych wzorców (** oznacza dowolny katalog w projekcie):
- **/IT*.java
- **/*IT.java
- **/*ITCase.java
Poniżej znajduje się przykładowa konfiguracja tego pluginu dodana do pliku pom.xml naszego projektu:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M4</version> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin>
Do katalogu src/test/java/com/kursjava/maven dodamy plik ITFactorialCounter.java z przykładowym testem:
package com.kursjava.maven; import static org.junit.Assert.assertEquals; import org.junit.Test; public class ITCheckFactorial { @Test public void shouldReturnFactorial() { assertEquals(24, FactorialCounter.factorial(4)); } }
Możemy teraz zlecić Mavenowi zbudowanie projektu wraz z wykonaniem testów integracyjnych przy użyciu mvn verify:
Jak widzimy powyżej, najpierw wykonane zostały testy jednostkowe przez plugin Surefire, a dopiero potem plugin Failsafe wykonał test integracyjny.
Plugin ten możemy konfigurować podobnie jak plugin Surefire. Możemy zmienić konfigurację pluginu, by szukał testów integracyjnych w innym podkatalogu czy też zmienić wzorce dopasowania plików z testami itp.
Podsumowanie¶
- Aby zbudować nasz projekt, Maven wykonuje ponad 20 zależnych od siebie faz (build lifecycle phases), w skład których wchodzą m. in. fazy: compile, test, package, oraz install.
- Pluginy Mavena to po prostu projekty Mavenowe, które udostępniają pewną funkcjonalność, z której możemy korzystać używając Mavena.
- Część z pluginów jest od razu używana przez Maven, jak na przykład plugin Maven Compiler, a inne musimy wpierw skonfigurować w pliku pom.xml.
- Z pluginów korzysta się podając nazwę pluginu oraz jedno z zadań (plugin goal), które może on wykonać, np. komenda mvn exec:java zleca wykonanie zadania java pluginu Exec.
- Fazy budowy projektu mogą mieć przypisane zadania pluginów. Gdy Maven wykonuje daną fazą, to uruchomi wszystkie zadania pluginów skojarzone z tą fazą. Dla przykładu, faza package uruchamia zadanie jar pluginu Maven Jar.
- Aby sprawdzić, jakie fazy budowy projektu są dostępne i zobaczyć ich domyślnie przypiasne zadania pluginów, skorzystaj z komendy:
mvn help:describe -Dcmd=install
- Aby zobaczyć informacje o pluginie i jego zadania, skorzystaj z poniższej komendy (wartość dla parametru plugin to złączenie groupId i artifactId pluginu):
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-surefire-plugin
- Domyślnie generowany przez Maven plik JAR w katalogu target zawiera jedynie klasy naszego projektu. Możemy skorzystać z pluginu Assembly, aby wygenerować jeden duży JAR zawierający wszystkie zależności.
- Plugin Assembly przed użyciem należy skonfigurować w pliku pom.xml:
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.2.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>
- Aby skorzystać z tego pluginu, korzystamy z komendy:
mvn compile assembly:single
- Wynikiem działania pluginu Assembly jest plik JAR w katalogu target o przykładowej nazwie policz-silnie-1.0-SNAPSHOT-jar-with-dependencies.jar.
- W pliku pom.xml możemy podłączyć zadania pluginów pod fazy budowy projektu. Dla przykładu, plugin Assembly mógłby zostać przypisany do fazy package:
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.2.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
- W fazie test, Maven domyślnie wykonuje testy zawarte w plikach projektu, których nazwa pasuje do któregoś z poniższych wzorców (** oznacza dowolny katalog projektu):
- **/Test*.java
- **/*Test.java
- **/*Tests.java
- **/*TestCase.java
- Testy powinniśmy umieszczać w katalogu src/test/java.
- Jeżeli mamy potrzebę korzystać z pliku z testami, który nie pasuje po powyższych wzorców, to możemy skonfigurować plugin Surfire, którego Maven używa do uruchamiania testów, aby brał pod uwagę pliki z testami o innych nazwach:
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> <configuration> <includes> <include>**/*Test.java</include> <include>CheckFactorial.java</include> </includes> </configuration> </plugin>
- Zauważmy, że dodaliśmy także <include> z **/*Test.java, ponieważ ręczna konfiguracja nazw plików testowych powoduje, że domyślne wzorce, które są wylistowane powyżej, przestają być używane.
- Pliki z testami możemy także wykluczać korzystając z elementu <exclude>.
- Możemy zlecić wykonanie testów z konkretnej klasy, przekazując jej nazwę jako wartość parametru test w trakcie wykonywania fazy o tej samej nazwie:
mvn -Dtest=CheckFactorial test
- Aby pominąć wykonanie testów należy ustawić parametr maven.test.skip:
mvn install -Dmaven.test.skip=true
- Poza testami jednostkowymi, Maven może także uruchamiać dla nas testy integracyjne w fazie verify. Domyślnie ta funkcjonalność nie jest włączona.
- Aby Maven wykonał testy integracyjne, musimy do pliku pom.xml dodać konfigurację pluginu o nazwie Failsafe. Plugin Failsafe domyślnie wykonuje testy w plikach, które pasują do któregoś z poniższych wzorców (** oznacza dowolny katalog w projekcie):
- **/IT*.java
- **/*IT.java
- **/*ITCase.java
- Poniżej znajduje się przykładowa konfiguracja tego pluginu dodana do pliku pom.xml naszego projektu:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M4</version> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin>
- W plikach pom.xml możemy używać nazwanych wartości, umieszając je w elemencie <properties>:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> <log4j.version>2.13.1</log4j.version> </properties>
- Aby odnieść się do nazwanej wartości, korzystamy ze składni ${nazwa}, na przykład:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j.version}</version> </dependency>
Zadania¶
Dodaj do swojego projektu klasę z testami, która będzie miała nazwę pasującą do jednego ze wzorców klas testowych używanych w Maven (np. niech klasa ta kończy się na słowo Test).
Skonfiguruj plugin Surefire tak, by klasa ta była wykluczana podczas uruchamiania testów w fazie test. Zajrzyj do oficjalnej dokumentacji po więcej informacji:
https://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html
Bardzo dobry, wyczerpujacy material. Dziekuje!
Świetny artykuł zarówno pod kątem treści jak i prezentacji. Brawa
Dziękuję 🙂