W moim najnowszym poście piszę o tym, w jaki sposób zintegrować RSpec, ngrok, Capybarę i CrossBrowserTesting.com żeby testować interfejs użytkownika zdalnie na różnych przeglądarkach i systemach.
Post można przeczytać na blogu Leadfeedera TUTAJ
Jan Bajena – DevBlog
W moim najnowszym poście piszę o tym, w jaki sposób zintegrować RSpec, ngrok, Capybarę i CrossBrowserTesting.com żeby testować interfejs użytkownika zdalnie na różnych przeglądarkach i systemach.
Post można przeczytać na blogu Leadfeedera TUTAJ
Ostatnio spędziłem kilka ładnych godzin pracując nad blogiem w Leadfeederze (blog stoi na Jekyllu). Moim celem było wyświetlenie customowego pliku w formacie Liquid. Pozornie prosta sprawa pochłonęła mnóstwo czasu. Z tego powodu postanowiłem opisać to w moim najnowszym poście tak, aby oszczędzić czasu innym „śmiałkom” podchodzącym do tego tematu.
Zapraszam do przeczytania w serwisie dev.to: LINK.
W najnowszym poście piszę o niedawno odkrytym przeze mnie gemie do obsługi DynamoDB w Ruby.
O tym jak zainstalować, skonfigurować i używać Dynamoida można przeczytać tutaj: https://dev.to/bajena/how-to-setup-dynamoid-in-ruby-projects-to-simplify-dynamodb-interactions-105d
Ostatnimi czasy pracowałem ze strumieniami w Redisie i trafiłem na następujący problem: jak usunąć N najstarszych elementów ze strumienia?
Okazuje się, że da się to zrobić w łatwy sposób za pomocą skryptu LUA. Zapraszam do przeczytania artykułu na dev.to: https://dev.to/bajena/removing-n-oldest-entries-from-a-redis-stream-5aob
W moim poprzednim poście opisałem moje wysokopoziomowe przemyślenia na temat frameworka Hanami. Dzisiaj w ramach programistycznego „mięska” postanowiłem opisać jak użyłem biblioteki hanami-ujs
podczas tworzenia mojej aplikacji Flashcard Genius.
Post można znaleźć na Medium.
Po kilku latach programowania w Ruby on Rails poczułem pewne wypalenie i zacząłem szukać jakiegoś urozmaicenia. Z tego powodu zdecydowałem się wziąć na warsztat jeden z konkurencyjnych Ruby’owych frameworków – Hanami.
Przez kilka miesięcy po godzinach bawiłem się Hanami i stworzyłem aplikację Flashcard Genius, która wspomaga mnie w nauce słówek z języka włoskiego. W najnowszym poście spisałem moje wrażenia na temat tego co podczas tworzenia tej aplikacji spodobało mi się w Hanami i o tym, czego mi w nim brakuje.
Jeśli trafiłeś na tą stronę to prawdopodobnie zmagasz się z tym samym problemem, co ja. Z tego posta dowiesz się trochę o tym, czym są kodowania znaków, skąd biorą się nieprawidłowe sekwencje bajtów i jak je naprawić.
Post można przeczytać pod tym adresem:
https://dev.to/bajena/solving-invalid-byte-sequence-in-utf-8-errors-in-ruby-1f27
Jeżeli ciekawi Cię, co oznacza powiedzenie „po ptokach” w kontekście programowania, to zachęcam do przeczytania mojego nowego wpisu na medium.
W najnowszym poście opisałem mój ostatni problem z dziedziczeniem zmiennych zdefiniowanych z użyciem słowa kluczowego ‚!default’ w plikach scss.
Artykuł można znaleźć pod tym linkiem: https://medium.com/@bajena3/a-story-about-overriding-scss-variables-and-the-default-keyword-eace1de34631
Ostatnio pisząc testy jednostkowe zauważyłem, że bardzo często powtarzam schemat „Napisz test” -> „Skopiuj test” -> „Zmień wartość w skopiowanym teście tak, by przetestować warunek odwrotny” -> „Zmień opis na odwrotny”.
W praktyce wygląda to mniej więcej tak:
Pomyślałem, że mogę nieco usprawnić ten proces, a przy okazji nauczyć się czegoś nowego – wpadłem więc na pomysł, że stworzę plugin do mojego ulubionego edytora – Sublime Text 3.
Plugin ten ma mieć jedno bardzo proste zadanie, a mianowicie zamienianie angielskich zdań w trzeciej osobie liczby pojedynczej z twierdzących na przeczące i na odwrót.
Skoro pomysł już jest, to teraz wystarczy go zrealizować, prawda? Problem jest tylko taki, że nie mam pojęcia, jak pisać pluginy do ST3. Szybkie googlowanko zaprowadziło mnie do tego artykułu. Stamtąd dowiedziałem się, że pluginy są po prostu pythonowymi klasami dziedziczącymi po sublime_plugin.TextCommand i odpalane są przez interpreter Pythona wbudowany w Sublime’a. Nie byłem tym zachwycony, bo nigdy nie pisałem w tym języku, jednak odnalazłem w nim sporo analogii do ruby’ego, a z większością problemów wynikających z nieznajomości API lub składni mogłem się uporać dzięki kochanemu StackOverflow.
Mimo że głównym elementem mojego pluginu są operacje na stringach, potrzebowałem również zapoznać się z podstawami API Sublime’a (Dokumentację można znaleźć tutaj). Interfejs nie jest super intuicyjny, ale do mojego celu wystarczyło zrozumieć kilka konceptów:
Po zapoznaniu się z tutorialem i podstawami API Sublime’a zacząłem zastanawiać się, jak napisać mój plugin…
Pierwszą rzeczą, którą musi zrobić nasz program będzie znalezienie w linii, w kórej aktualnie znajduje się kursor Regionu obejmującego zdanie w pojedynczych (‚ ‚) lub podwójnych (” „) apostrofach. Aktualna pozycja kursora musi zawierać się w tym regionie. Postanowiłem, że najlepiej do tego sprawdzi się wyrażenie regularne.
Przy budowaniu wyrażeń regularnych bardzo przydatna jest strona https://pythex.org/ – zaoszczędziłem dzięki niej mnóstwo czasu.
Zacząłem od następującego wyrażenia:
(\”.*\”|(\’.*\’))
To wyrażenie jest niezłe, bo pozwala na dopasowanie zarówno do pojedynczych, jak i podwójnych apostrofów ale ma jedną wadę – jest „zachłanne”. Oznacza to, że w wypadku, gdy linia zawiera kilka stringów w apostrofach nasze wyrażenie znajdzie cały ciąg znaków znajdujący się pomiędzy pierwszym i ostatnim apostrofem w danym zdaniu. Czyli zamiast „It requires a name” and „It requires a surname„ wyszuka „It requires a name” and „It requires a surname”. Na szczęście python posiada proste narzędzie, pozwalające to naprawić:
(\”.*?\”|(\’.*?\’))
Teraz wyrażenie działa tak, jakbyśmy tego oczekiwali.
Niestety moja radość nie trwała długo – kolejnym problemem, na który się natknąłem, był fakt, że moje wyrażenie nie radzi sobie z tzw. escaped quotes, czyli z pojedynczym apostrofem poprzedzonym znakiem backslash. Tutaj znowu python daje radę – zgodnie z regular expression cheatsheet ze strony pythex.org aby odfiltrować apostrofy poprzedzone backslashem można wykorzystać negative lookbehind assertion:
(?<!…)
Finalnie wyrażenie wygląda tak:
(\”.*?\”|((?<!\\)\’.*?(?<!\\)\’))
Po zbudowaniu odpowiedniego wyrażenia musiałem już tylko spośród dopasowanych ciągów znaleźć taki, którego region zawiera pozycję kursora:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def find_quotes(self): | |
quotes_regex = r'(\".*?\"|((?<!\\)\'.*?(?<!\\)\'))' | |
iterator = re.finditer(quotes_regex, self.current_line()) | |
for match in iterator: | |
quote_span = match.span() | |
region = sublime.Region(quote_span[0] + self.current_line_start(), quote_span[1] + self.current_line_start()) | |
if region.contains(self.cursor_position): | |
return (quote_span[0], quote_span[1]) |
Po znalezieniu zdania będziemy chcieli zamienić je na przeczące. Metoda, którą wymyśliłem jest dość naiwna, ale jak dobrze wiemy jeżeli coś jest głupie a działa, to nie jest głupie 🙂
Najpierw przeanalizujmy sobie, jakie są możliwe czasowniki w czasie present simple dla podmiotów w 3. osobie i liczbie pojedynczej i w jaki sposób je zanegować:
Kod klasy SentenceNegator wygląda następująco:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SentenceNegator: | |
IRREGULAR_ES_VERB_ENDINGS = ["ss", "x", "ch", "sh", "o"] | |
CONSONANTS = ['a', 'e', 'i', 'o', 'u', 'y'] | |
def negate(self, sentence): | |
# is | |
if sentence.find("isn't") > –1: | |
return sentence.replace("isn't", "is") | |
if sentence.find("isn\\'t") > –1: | |
return sentence.replace("isn\\'t", "is") | |
if sentence.find("is not ") > –1: | |
return sentence.replace("is not ", "is ") | |
if sentence.find("is ") > –1: | |
return sentence.replace("is ", "is not ") | |
# has | |
if sentence.find("does not have") > –1: | |
return sentence.replace("does not have", "has") | |
if sentence.find("doesn't have") > –1: | |
return sentence.replace("doesn't have", "has") | |
if sentence.find("doesn\\'t have") > –1: | |
return sentence.replace("doesn\\'t have", "has") | |
if sentence.find("has ") > –1: | |
return sentence.replace("has ", "does not have ") | |
# should | |
if sentence.find("shouldn't") > –1: | |
return sentence.replace("shouldn't", "should") | |
if sentence.find("shouldn\\'t") > –1: | |
return sentence.replace("shouldn\\'t", "should") | |
if sentence.find("should not") > –1: | |
return sentence.replace("should not", "should") | |
if sentence.find("should") > –1: | |
return sentence.replace("should", "should not") | |
# must | |
if sentence.find("mustn't") > –1: | |
return sentence.replace("mustn't", "must") | |
if sentence.find("mustn\\'t") > –1: | |
return sentence.replace("mustn\\'t", "must") | |
if sentence.find("must not") > –1: | |
return sentence.replace("must not", "must") | |
if sentence.find("must ") > –1: | |
return sentence.replace("must ", "must not ") | |
# can | |
if sentence.find("can't") > –1: | |
return sentence.replace("can't", "can") | |
if sentence.find("can\\'t") > –1: | |
return sentence.replace("can\\'t", "can") | |
if sentence.find("cannot") > –1: | |
return sentence.replace("cannot", "can") | |
if sentence.find("can ") > –1: | |
return sentence.replace("can ", "cannot ") | |
# doesn't work -> works | |
doesnt_regex = r'(doesn\'t|doesn\\\'t|does not) (?P<verb>\w+)' | |
if re.search(doesnt_regex, sentence): | |
def replace_doesnt(matchobj): | |
verb = matchobj.group(2) | |
if verb.endswith("y") and self.__is_consonant(verb[–2]): | |
return "{0}ies".format(verb[0:–1]) | |
for ending in self.IRREGULAR_ES_VERB_ENDINGS: | |
if verb.endswith(ending): | |
return "{0}es".format(verb) | |
return "{0}s".format(verb) | |
return re.sub(doesnt_regex, replace_doesnt, sentence, 1) | |
verb_regex = r'(It |it |)(?P<verb>\w+)s( |$)' | |
# works -> does not work | |
def replace_verb(matchobj): | |
subject = matchobj.group(1) | |
verb = matchobj.group(2) | |
whitespace = matchobj.group(3) | |
# flies -> fly, but not die -> dy | |
if verb.endswith("ie") and len(verb) > 3: | |
verb = "{0}y".format(verb[0:–2]) | |
# stresses -> stress | |
for ending in self.IRREGULAR_ES_VERB_ENDINGS: | |
if verb.endswith("{0}e".format(ending)): | |
verb = verb[0:–1] | |
return "{0}does not {1}{2}".format(subject, verb, whitespace) | |
if re.search(verb_regex, sentence): | |
return re.sub(verb_regex, replace_verb, sentence, 1) | |
return sentence | |
def __is_consonant(self, letter): | |
return letter not in self.CONSONANTS |
Zasada jest prosta – dla wyżej wymienionych typów czasowników należy wykonać takie kroki:
Warto wspomnieć, że takie podejście sprawdzi się tylko dla zdań jednokrotnie złożonych, niezawierających conditionali. Czyli np. „It has a value” zostanie poprawnie zmienione w „It does not have a value”, ale już „It has a value if other value is 0” może dać nieoczekiwany rezultat, czyli: „It has a value if other value is not 0”. W takim przypadku nasz program musiałby znać kontekst zdania, żeby móc je poprawnie zanegować.
Pierwszą rzeczą jaką zrobiłem zaraz po zapoznaniu się z API Sublime’a było zadanie sobie pytania „jak mam to przetestować?”. Stwierdziłem, że przy mojej niskiej znajomości Pythona szansa na wprowadzenie błędu przy którejś z kolei zmianie jest całkiem duża, a dobre pokrycie testami da mi pewność, że nic po drodze nie zepsuję (poza tym to po prostu dobra praktyka).
Tutaj z pomocą przyszła mi biblioteka https://github.com/randy3k/UnitTesting. Działa ona w bardzo prosty sposób – symuluje realne użycie naszego pluginu poprzez stworzenie nowego okienka edytora i wykonanie naszej komendy. Po odpaleniu komendy możemy sprawdzić czy tekst w oknie testowym jest zgodny z oczekiwanym.
Plik z zestawem testów należy umieścić w folderze tests naszej paczki.
Przykładowy plik z testem jednostkowym wygląda tak:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sublime | |
import sys | |
from unittest import TestCase | |
class TestNegateSentence(TestCase): | |
def setUp(self): | |
self.view = sublime.active_window().new_file() | |
# make sure we have a window to work with | |
s = sublime.load_settings("Preferences.sublime-settings") | |
s.set("close_windows_when_empty", False) | |
def tearDown(self): | |
if self.view: | |
self.view.set_scratch(True) | |
self.view.window().focus_view(self.view) | |
self.view.window().run_command("close_file") | |
def test_negate_is(self): | |
self.check_substitution('"The dog is black"', '"The dog is not black"') | |
def check_substitution(self, input, expected): | |
self.set_text(input) | |
self.view.run_command("negate_sentence") | |
self.assertEqual(self.get_text(), expected) | |
def set_text(self, string): | |
self.view.run_command("insert", {"characters": string}) | |
def get_text(self): | |
return self.view.substr(self.view.line(self.view.text_point(0, 0))) | |
def move_cursor(self, position): | |
pt = self.view.text_point(0, position) | |
self.view.sel().clear() | |
self.view.sel().add(sublime.Region(pt)) |
Po dodaniu testów do projektu pomyślałem, że fajnym usprawnieniem będzie automatyczne uruchamianie testów w którymś z serwisów umożliwiających CI. Okazało się, że w repozytorium https://github.com/randy3k/UnitTesting można znaleźć gotową konfigurację do systemu Travis CI. Integracja z Travis CI odbyła się bezboleśnie – już pierwszy build zaświecił się na zielono.
Poza integracją z CI postanowiłem również zintegrować się z codecov – serwisem pokazującym w jakim stopniu nasz projekt jest pokryty testami. Ponownie okazało się to banalne, ponieważ plik .travis.yml zawiera konfigurację, która automatycznie generuje plik z pokryciem testami. Jedyne co musiałem zrobić, to zalogować się w codecov.io i wskazać odpowiednie repozytorium z GitHuba.
Dzięki dodaniu ciągłej integracji i pokrycia testami mam pewność, że każda linia jest przetestowana, a mój projekt działa jak należy.
Oprócz tego zyskałem też takie czadowe badge, które dodałem do repozytorium w GitHubie:
Czas zobaczyć plugin w akcji. Można go przetestować wykorzystując jeden ze sposobów:
Poniżej mała prezentacja:
Napisanie własnego pluginu do edytora, w którym codziennie spędza się po 8 godzin daje sporą satysfakcję, nawet jeżeli jest to super proste narzędzie.
Poza samą satysfakcją takie proste zadanko idealnie sprawdziło się jako poligon służący do nauki Pythona. Dwie pieczenie na jednym ogniu!
Całość projektu można znaleźć pod adresem https://github.com/Bajena/SublimeNegateSentence