13. listopadu 2017

vimdiff, nástroj drsňáků


Musím se vám k něčemu přiznat... Už patnáct let je Vim můj nejoblíbenější textový editor. A občas, čas od času, i hlavní nástroj na programování.

Umím si poeditovat vimrc, který po léta udržuju a vylepšuju. Dokonce jsem se i naučil trochu Vim script/VimL a napsal dva zanedbatelné a nedotažené pluginy (pro Gradle a WSDL).

Ale vždycky jsem se jako čert kříži vyhýbal jedné věci - používání vimdiff. Nicméně na každého jednou dojde. Z určitých (pro článek nepodstatných) důvodů jsem si nemohl pro nové vývojové prostředí nastavit P4Merge a tak jsem vstoupil do zapovězené komnaty.

Disclaimer: Tenhle článek píšu jako shrnutí toho, jak jsem práci s vimdiff pochopil. Pokud máte víc zkušeností, budu rád, když se podělíte v komentářích.

2-way merge

Nejjednodušší způsob, jak používat vimdiff - pokud pomineme, že umí dělat i "plain old" diff - je 2-way merge: máme vedle sebe dvě okna se zvýrazněnými rozdíly a chceme mezi nimi tyto změny propagovat.

vimdiff, 2-way merge

Stav na předešlém obrázku, který je výchozí pro merge, se dá dosáhnout několika způsoby:
  • Příkazem: vimdiff myFile.txt theirFile.txt
  • Příkazem: vim -d myFile.txt theirFile.txt
  • Kombinací příkazů:
    • vim myFile.txt
    • :diffsplit theirFile.txt
  • Kombinací příkazů
    • vim -O myFile.txt theirFile.txt (vsplit obou souborů)
    • :diffthis (zapne diff na aktuálním bufferu)
    • Ctrl-W Ctrl-W (skok do druhého bufferu)
    • :diffthis(zapne diff v druhém bufferu)

Základní příkazy

Tak, diff máme zobrazený, co s ním? První věc - je potřeba se v diffu umět pohybovat. Kromě toho, že můžete použít jakýkoli skok, který znáte z běžného Vimu, jsou tu dva příkazy, které umožňují skákat po jednotlivých rozdílech:
  • ]c skočí na následující diff
  • [c skočí na předcházející diff

Za druhé - chceme propagovat změny z/do aktuálního bufferu: skočíme na diff, který chceme upravit a:
  • do, nebo :diffget natáhne změny z "druhého" bufferu do toho aktuálního.
  • dp, nebo :diffput propaguje změny z aktuálního bufferu do "toho druhého".

Za třetí - změny uložíme. Kromě příkazů na standardní ukládání (:w, ZZ atd.) se může hodit:
  • :only zavře všechny ostatní buffery kromě toho aktuálního
  • :qall zavře všechny otevřené buffery
  • :only | wq zavře ostatní buffery + uloží stávající + ukončí Vim. Cool!

Eventuálně začtvrté - pokud věci nejdou hladce, může se šiknout:
  • :diffupdate znovu proskenuje a překreslí rozdíly (u komplikovanějších mergů nemusí Vim správně pochopit danou změnu)
  • :set wrap nastavení zalamování řádků (hodí se při velmi dlouhých řádcích, typicky některá XML)
  • zo/zc otevře/zavře skryté (folded) řádky

3-way merge

Nemusím vám říkat, že 2-way merge je pro školáky - profíci makaj v Gitu, či v Mercurialu a tam je dvoucestný merge nedostačující. Ke slovu přichází 3-way merge. Co to je?

Schéma 3-way merge

3-way merge není nic složitého. V podstatě jde o to, že máme dvě verze, které mají společného předka. V mergovacím nástroji pak vidíme všechny tři verze vedle sebe a většinou máme k dispozici ještě čtvrté okno s aktuálním výsledkem merge.

3-way merge v aplikaci


Nastavení Gitu

Nastavení spolupráce Gitu a vimdiff je jednoduché - stačí spustit z příkazové řádky následující sadu příkazů:
$ git config --global merge.tool vimdiff
$ git config --global merge.conflictstyle diff3
$ git config --global mergetool.prompt false
$ git config --global mergetool.keepBackup false

Pokud se podíváte do ~/.gitconfig, měli byste tam vidět:


Nastavení Mercurialu

Nastavení Mercurialu je podobně jednoduché. Otevřeme soubor ~/.hgrc příkazem
$ hg config --edit
a vložíme následující řádky


Sekce [extensions] a [extdiff] nejsou pro merge nutné, ale hodí se, pokud chceme vimdiff používat jako dodatečný externí diff nástroj. Sekundární diff spustíme příkazem hg vimdiff.

Základní příkazy

Základní příkazy jsou stejné jako v sekci 2-way merge, s výjimkou příkazů dp/do (:diffput/:diffget) - pokud bychom je nyní použili, vimdiff nám zahlásí chybu:
More than two buffers in diff mode, don't know which one to use
To je v pořádku: u 3-way merge se ve vimdiff otevřou 4 buffery, všechny v diff módu. Takže do té doby, než se vimdiff naučí komunikovat telepaticky, je potřeba mu říct, ze kterého bufferu chceme danou změnu natáhnout.

vimdiff, 3-way merge v Gitu

Klasické merge flow vypadá následovně:
  1. Začínáme v dolním "výsledkovém" bufferu.
  2. ]c (skočit na následující diff, který chceme mergovat)
  3. :diffget <identifikace-bufferu> získáme změnu z daného bufferu (viz dále)
  4. Opakujeme 2-3.
  5. :only | wq uložíme merge.

Obecně, identifikátor bufferu získáme příkazem :ls. To je ale dost nepraktické a nepřehledné. Další možnost je identifikovat buffer částečným názvem souboru. Tady přichází na pomoc jak Git, tak Mercurial, který přidávají k názvům souborů příhodný suffix.

Merge v Gitu

Git přidává do názvů mergovaných souborů následující suffixy, v pořadí zleva doprava: LOCAL (vlevo), BASE (uprostřed), REMOTE (vpravo). Pro natažení změny z (levého) bufferu LOCAL můžeme použít příkaz :diffg LO.

Výpis bufferů pro Git:
:ls
  1 #a   "./myFile_LOCAL_7424.txt"      line 1
  2  a   "./myFile_BASE_7424.txt"       line 0
  3  a   "./myFile_REMOTE_7424.txt"     line 0
  4 %a   "myFile.txt"                   line 12

Merge v Mercurialu


vimdiff, 3-way merge v Mercurialu

Mercurial přidává do názvů mergovaných souborů následující suffixy, v pořadí zleva doprava: orig (vlevo), base (uprostřed), other (vpravo). Pro natažení změny z (levého) bufferu orig můžeme použít příkaz :diffg orig.

Výpis bufferů pro Mercurial:
:ls
  1 %a   "myFile.txt"                   line 2
  2  a-  "myFile.txt.orig"              line 0
  3  a-  "/tmp/myFile.txt~base.iZwwuA"  line 0
  4  a-  "/tmp/myFile.txt~other.km9Itr" line 0

Co mi (zatím) schází?

Musím říct, že potom, co jsem si vimdiff osahal, pochopil jeho logiku a naučil se jeho příkazy, jsem si ho docela oblíbil.

Jediná výtka zatím jde za jeho neschopností skákat přímo po konfliktech - Git i Mercurial dělají výborně automatické merge a ty jsou samozřejmě vidět ve vimdiffu taky, jako pouhá změna bez konfliktu. Mít nějaký příkaz, který rozlišuje pouhý diff a konflikt, by bylo fajn.

Související články


1. listopadu 2017

Trampoty s JUnit 5

Poslední dobou jsem nepsal moc unit testy... v Javě. Jednak jsem posledního půl roku hodně prototypoval - a tam moc testů nenapíšete - a když už jsem testy psal, tak to bylo převážně ve Scale, nebo v Clojure.

Teď ale naše firma projevila sklony k evoluci, se snahou trochu více zautomatizovat vytváření prostředí a zakládání projektů. Sice to jde mimo mě, ale když jsem byl požádán, ať napíšu testovací projekty v Javě pro Gradle a Maven, chopil jsem se příležitosti a ponořil se do (povrchního) studia JUnit 5.

Vyznání

Obecně musím říct, že pro JUnit mám slabost - začal jsem ho používat na začátku své Java kariéry ve verzi 4.2 (pro pamětníky únor 2007) a tak vlastně celý můj Java-produktivní věk jsem strávil se čtyřkovou verzí. Naučilo mě to hodně - za to, že jsem dnes takový skvělý programátor (ha, ha, ha) vděčím tomu, že mě unit testy naučily psát dobrý design.

Samozřejmě jsem si k tomu občas něco přibral. Už v roce 2008 jsem si mistrně osvojil (tehdy progresivní) jMock 2 a naplno se oddával neřesti BDD. Taktéž TestNG jsem si na pár projektech zkusil. Ale gravitační síla tradičního JUnit a TDD mě vždy přivedla zpátky.

A teď zažívám něco jako déjà vu. Je to podobný, jako když přišla Java 5 - skoro všechny nástroje s tím mají menší nebo větší problém. Java komunita to ještě moc neadaptovala. Když narazíte na problém, StackOverflow často nepomůže. Atd.


Pár aktuálních problémů JUnit 5 se mi podařilo vyřešit ke své spokojenosti. Tady je máte na stříbrném podnose.

Zadání

Zadání, které jsem dostal, bylo triviální - napsat miniaturní Java projekt, buildovatelný Gradlem a Mavenem, který bude mít unit testy. Projekt se bude buildovat na Jenkinsu, potřebuje změřit pokrytí testy pomocí JaCoCo a projít statickou analýzou kódu na SonarQube.

Jak říkám, bylo by to triviální, kdybych si pro testy nevybral JUnit 5.

Gradle

Odpírači pokroku a milovníci XML se mnou nebudou souhlasit, ale já považuju Gradle za základ moderní automatizace na JVM. Včetně (a primárně) buildů. Jak tedy zkrotit Gradle, aby se kamarádil s JUnit 5?

Zatím jsem se v tom nějak moc nevrtal, pač nemám ambice se stát JUnit 5 guru, jen potřebuju běžící testy. Ale je dobré vědět, že:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage (JUnit 5 User Guide)
JUnit Vintage je pro JUnit 4, což nás dnes nezajímá. Zbývá tedy JUnit Platform pro spouštění unit testů a JUnit Jupiter pro samotné psaní testů.

Protože JUnit 5 změnilo pravidla hry, nestačí do Gradlu jenom přidat závislosti - současný Gradle novým unit testům nerozumí a zůstaly by nepovšimnuty. Naštěstí je k dispozici je nový plugin, který přidá do build life-cyclu nový task junitPlatformTest a který umí testy spustit.

Bohužel, plugin ještě pořád není dostupný na Gradle Plugins Portal, ale jen v Maven Central. Tím pádem se zatím nedá použít Plugins DSL :-(

V následujícím minimalistickém Gradle skriptu si povšimněte různých konfigurací pro jednotlivé závislosti:
  • testCompile pro api
  • testRuntime pro engine.


Závislost apigurdian-api je optional a je tam jenom proto, aby se ve výstupu nevypisovalo varování:
warning: unknown enum constant Status.STABLE
    reason: class file for org.apiguardian.api.API$Status not found

Task juPlTe je "zahákovaný" na standardní test task, který se dá použít také.

Spuštění JUnit 5 testů Gradlem

Jedna z killer feature Gradlu je incremental build - pokud nešáhnete na produkční kód, nebo na testy, Gradle testy nespouští. Je prostě chytrej ;-)

Gradle incremental build přeskočí testy, pokukd se kód nezměnil


Maven

Tradicionalisti milují Maven a protože jsem shovívavý lidumil, podělím se i o toto nastavení. Pro Maven platí totéž, co pro Gradle:
  • nový plugin (přesněji Surfire provider)
  • závislost na api a engine v různém scopu


Velký rozdíl mezi Mavenem a Gradlem je, že Maven incremental build (moc dobře) neumí - tupě spouští testy, kdykoliv mu řeknete.

Spuštění JUnit 5 testů Gradlem


JaCoCo pokrytí testy

Zbuildovat a spustit JUnit 5 testy byla ta jednodušší část. S čím jsem se trochu potrápil a chvilku jsem to ladil, bylo pokrytí testy. Vybral jsem JaCoCo, protože mi vždycky přišlo progresivnější, než Cobertura (jen takový pocit, či preference).

Dále budu uvádět jen nastavení pro Gradle, protože Maven je hrozně ukecaný. Pokud vás ale Maven (ještě pořád) zajímá, podívejte se do pom.xml v projektové repository.

Zkrácená JaCoCo konfigurace vypadá takto:


V předešlém výpisu jsou podstatné tři věci: (1) generování JaCoCo destination file je svázáno s taskem junitPlatformTest. (2) Definujeme název destination file. Název může být libovolný, ale aby fungovalo generování JaCoCo reportů, je potřeba, aby se soubor jmenoval test.exec. A za (3), pokud chceme některé soubory z reportu exkludovat, dá se to udělat trochu obskurně přes life-cycle metodu afterEvaluate. (Tohle by chtělo ještě doladit.)

JaCoCo pokrytí testy


SonarQube statická analýza

Sonar vlastně s JUnit nesouvisí. Pokrytí testy už máme přece vyřešeno. No, uvádím to proto, že opět je potřeba jít tomu štěstíčku trochu naproti.

Zkrácená verze Sonar konfigurace je následující (Maven opět hledejte v repo):


Tady jsou důležité dvě věci: (1) říct Sonaru, kde má hledat coverage report (klíč sonar.jacoco.reportPath) a za (2) naznačit, co má Sonar z coverage ignorovat (sonar.coverage.exclusions) - bohužel, JaCoCo exkluduje jenom z reportu, v destination file je všechno a tak to Sonaru musíte říct ještě jednou.

SonarQube statická analýza


Má smysl migrovat?

Jak je vidět, popsal jsem spoustu papíru, není to úplně easy peasy. A tak se nabízí hamletovská otázka: má smysl upgradovat z JUnit 4 na verzi 5?


Výše už jsem zmínil velmi přesnou analogii s Javou 5. Tehdy šlo hlavně o anotace a generické kolekce. Můj povrchní dojem je, že u JUnit 5 může být tahákem Java 8 (na nižších verzích Javy to neběží), takže primárně lambdy a streamy.

Pokud máte stávající code base slušně pokrytou pomocí JUnit 4, tak se migrace nevyplatí. Protože ale JUnit 5 umí (pomocí JUnitPlatform runneru) spouštět obě verze simultánně, je možné na verzi 5 přecházet inkremenálně.

Projekt repository

Na Bitbucket jsem nahrál repozitory jednoduchého projektu, kde si můžete v Gradlu a v Mavenu spustit JUnit 5 testy, vygenerovat JaCoCo report a publikovat výsledek do SonarQube.