Piotr Sarnacki home

Ruby debug

Debugger to bardzo ciekawe narzędzie. Bywa niezwykle pomocny przy różnego rodzaju problemach ciężkich do rozwiązania manualnie, ale stosunkowo niewiele osób go używa. I nawet jeżeli ktoś wie o jego istnieniu i wie jak z niego korzystać, to bez takiego nawyku, wiedza ta na niewiele się zdaje. Wyobraźmy sobie taką sytuację. Próbuję poprawić buga znalezionego w kodzie, więc piszę padający test i zaczynam wprowadzanie poprawki. Nie widzę żadnej anomalii patrząc w kod, więc zaczynam przyglądać mu się bliżej. I wbrew temu co mogą niektórzy powiedzieć, ten kod wcale nie musi być skomplikowany. Ruby jest językiem dynamicznie typowanym, więc bardzo często błędy w kodzie biorą się stąd, że dostajemy nie to co chcieliśmy. W takim wypadku, nawet przy bardzo prostym kodzie, możemy siedzieć nad rozwiązaniem dłuższą chwilę, bo umysł podświadomie omija problematyczne miejsce zakładając, że dostajemy oczekiwaną wartość. Wracając do poprawki, zaczynam wstawiać w różne miejsca w kodzie metody, które wyświetlą dane pomocne przy debugowaniu, najczęściej robię to w taki sposób:

def method_with_bug(value)

p [:value, value]

# .... end

Dzięki użyciu metody p wyświetlona wartość będzie jeszcze potraktowana inspect. Jeżeli kilka takich linijek załatwi sprawę, to jesteśmy w domu, ale jeżeli nie? Najprawdopodobniej po każdym kolejnym dodanym p trzeba będzie uruchomić jeszcze raz test i być może okaże się, że będziemy musieli sprawdzić kilkanaście różnych rzeczy.

Debugger ma nad tego typu podejściem dość dużą przewagę - kiedy idziemy przez kod wykonując kolejne kroki, łatwiej jest zauważyć, że coś jest nie tak. Wyświetlając część danych na ekran, nie wiemy tak naprawdę gdzie siedzi błąd, więc trochę strzelamy. I nie chcę przez to powiedzieć, że przy każdym błędzie opłaca się odpalać debuggera, Warto po prostu mieć nawyk jego używania. Ja z reguły zaczynam od wyświetlenia kilku wartości, które są najbardziej podejrzane i mogą mi pokazać gdzie leży błąd i jeżeli to nic nie daje, to używam debuggera. Nawyk jest tutaj bardzo ważny, bo bez niego najczęściej skończy się na dodawaniu kolejnych puts, za każdym razem poprzedzonych myślą: "Ok, już wiem o co chodzi, tylko jeszcze wyświetlę ten obiekt, nie ma sensu włączać ruby-debug".

Jeżeli chodzi o przeciwników debuggera, to jednym z głównych zarzutów jest: "Używając debuggera można uniknąć dyscypliny, która powinna towarzyszyć nam przy programowaniu i tym samym ominąć kroki, które bez debuggera trzeba by wykonać: pisanie testów, refactoring, dodanie dokumentacji". Zgadzam się z tym, że w przypadku kiedy kod jest marnej jakości, nie ma do niego testów i dokumentacji, to użycie debuggera bez poprawy sytuacji jest błędem. Problem w tym, że sytuacja nie zawsze tak wygląda i debugger wcale nie oznacza porzucenia testów (o czym będzie jeszcze za chwilę). Jeżeli chodzi o refaktoryzację, to też jestem zwolennikiem jak najczęstszego jej stosowania, ale to nie zawsze jest możliwe. Czasami ze względu na czas, czasami ze względu na dużą ilość kodu, czasami ze względu na małą wiedzę o kodzie (np. stara aplikacja, którą musimy utrzymywać). Ale żeby nie przedłużać zostawię tego typu rozważania i przejdę do sedna.

Do czegu używam debuggera?

Użycie debuggera wygląda z reguły w ten sposób, że w momencie kiedy chcemy poprawić jakiś problem w aplikacji, to wstawiamy tam debugger, odpalamy aplikację (albo w przypadku aplikacji np. w railsach odświeżamy stronę w przeglądarce) i sprawdzamy co się dzieje. Ja prawie nigdy nie robię tego w ten sposób. Najczęściej używam debuggera przy okazji uruchamiania testów. Po znalezieniu problemu piszę padający test i wtedy jeżeli mam problem z poprawieniem kodu, uciekam się do użycia debuggera.

Kolejny dość ciekawy przypadek, przy którym może się przydać debugger, to sprawdzenie co właściwie robi jakaś metoda z biblioteki, której używamy. Czasami kod zewnętrznych bibliotek nie jest zbyt dobry i wtedy szybko kapituluję, ale bardzo często już na samym początku sprawdzane są różnego rodzaju opcje, co daje dość dużą wiedzę o tym co można do niej przekazać. Jest to też bardzo przydatne przy rozszerzaniu jakiegoś API. Railsy 3 mają na przykład bardzo fajne API w kontrolerach i jeżeli wiemy co tam się dzieje, to można stosunkowo łatwo rozszerzyć dany kontroler. Przydaje się więc możliwość prześledzenia co właściwie dzieje się np. po wykonaniu metody render

Ostatnią rzeczą, przy której często używam debuggera, jest prototypowanie i wszelkiego rodzaju "szybkie" skrypty. Przypuśćmy na przykład, że chcemy sparsować jakąś stronę w nokogiri i wyciągnąć z niej jakieś informacje. Najczęściej bardzo przydaje się przy takich zadaniach interaktywna konsola, w której możemy wypróbować wyciąganie różnych elementów. Problem w tym, że jeżeli zaczniemy od irba, to musimy tam przekleić część używanego skryptu, który otwiera stronę, tworzy dokument nokogiri i ewentualnie inne kawałki, które są nam potrzebne. Z kolei metoda debugowania polegająca na wyświetlaniu wszystkiego na ekran jest dość uciążliwa, szczególnie po dwudziestym wykonaniu tego samego skryptu. Debugger łączy zalety obu podejść. Możemy skorzystać z przygotowanego kodu i zacząć w miejscu, w którym wszystko jest już ustawione i w tym samym czasie użyć interaktywnej konsoli.

Jak używać debuggera?

Na początku musimy debugger załadować, tzn. dodajć linijkę require 'ruby-debug' gdzieś przed miejscem, które chcemy sprawdzić. Następnie w dowolnym miejscu w kodzie możemy wstawić metodę debugger. Po uruchomieniu programu, w momencie kiedy interpreter dojdzie do tego miejsca, uruchomiona zostanie konsola, w której możemy sprawdzać aktualny stan procesu i poruszać się wykonując kolejne linijki kodu.

Przed rozpoczęciem pracy polecam stworzyć plik ~/.rdebugrc, w którym można umieścić domyślny config, ja używam:

set autolist # automatyczne wyświetlenie kodu po każdej komendzie
set autoeval # automatyczne wykonanie wpisanego kodu rubiego,
             # bez tego można używać tylko metody p i jako
             # argument podawać kod rubiego

Jeżeli używasz rubiego w wersji 1.9.3, to na końcu tego posta są instrukcje uruchomić ruby-debug pod tą wersją.

Najczęściej używane komendy

Komendy mają z reguły skrócone wersje, dlatego będę tutaj używał wersji skróconych, a rozszerzenie podawał w nawiasie kwadratowym, np. n[ext]. Jeżeli dana komenda przyjmuje jakieś argumenty, to będą one umieszczone po nazwie metody, nawias kwadratowy oznacza, że są opcjonalne.

Pełna lista komend jest dostępna po wykonaniu help, dokładniejszy opis każdej metody jest dostępny po wywołaniu help nazwa-instrukcji

To by było na tyle jeżeli chodzi o teorię, teraz przejdźmy do praktyki. Ze względu na to, że opisywanie tego jest lekko uciążliwe i pewnie średnio czytelne, wreszcie miałem okazję, żeby nagrać screencasta. Przejdę w nim przez kilka podstawowych przypadków, które napotkamy przy debugowaniu. Jeżeli będę miał trochę wolnego czasu, to dorzucę jeszcze drugą część, w której pokażę kilka scenariuszy wymienionych powyżej.

Ruby 1.9.3

Niestety gemy, które są wymagane do działania ruby-debug nie są cały czas publicznie wypuszczone, więc nie możemy ich prosto wylistować w Gemfile. Dlatego hostuję mały serwer rubygems, na którym je umieściłem. Jeżeli używasz rubiego w wersji 1.9.3, to używając bundlera można dopisać do Gemfile taki kod:

# source dla rubygems też powinno zostać
source 'http://gems.developers.stoliczku.pl'

group :development, :test do gem 'linecache19', '0.5.13' gem 'ruby-debug-base19', '0.11.26' gem 'ruby-debug19', :require => 'ruby-debug' end

Dodakowo, jeżeli używamy RVM, to trzeba podać bundlerowi ścieżkę do źródeł:

bundle config build.linecache19 --with-ruby-include="${MY_RUBY_HOME/rubies/src}"
bundle config build.ruby-debug-base19 --with-ruby-include="${MY_RUBY_HOME/rubies/src}"

Trochę tego niestety jest, ale po wykonaniu tych kroków wszystko działa bardzo dobrze.


If you liked this post consider following me on twitter.
blog comments powered by Disqus
Fork me on GitHub