Tag: RCov

Błąd “Errno::EMFILE: Too many open files” w testach jednostkowych

Dzisiaj pisząc kolejną porcję testów do Senpuu v5, po odpaleniu ich pod RCovem i Rubym 1.8.7 napotkałem na taki oto błąd:

Errno::EMFILE: Too many open files

Błąd ten związany jest z ilością otwartych jednocześnie plików (co ciekawe nie wystąpił podczas odpalania testów pod 1.9.2). Okazuje się, że tego typu wywołania:

file = File.new(File.join(Rails.root, "sciezka/do/pliku"), 'rb')

nie są nigdy zamykane, więc otwierając przykładowo 100 plików, żaden z nich nigdy nie zostanie zamknięty (przy zamykaniu aplikacji zostana). Z czasem ilość otwartych plików narasta i po pewnym czasie następuje zgłoszenie wyżej wspomnianego wyjątku. Rozwiązanie jest nader proste: zamykajmy otwierane pliki :)

W przypadku aplikacji jest to dość proste (robimy coś na pliku, kończymy i zamykamy). Jak jednak zapanować nad otwartymi plikami w testach? Przyznam szczerze, że jestem zbyt dużym leniem aby pamiętać o zamykaniu każdego pliku otworzonego w każdym teście. Z tego względu warto zbudować sobie prosty garbage collector który zajmie się tym za nas. Jak to zrobić pokaże na przykładzie podpięcia takiego kodu pod ActiveSupport::TestCase. Nic nie stoi na przeszkodzie aby tę metodę stosować także dla innych frameworków jak np. RSpec.

W metodze setup deklarujemy zmienną instancyjną, która przechowywać będzie nasze otwarte pliki:

  def setup
    super
    # Jakis inny kod ...
    @opened_files = []
  end

W metodzie teardown zamykamy wszystko:

  def teardown
    super
    # Jakis inny kod
    @opened_files.each { |f| f.close }
  end

I ostatnia z metod - metoda otwierająca pliki i zapamiętująca je w naszej zmiennej:

  def open_file(file = nil)
    return nil if filename.nil?
    file = File.new(File.join(Rails.root, file), 'rb')
    @opened_files << file
    file
  end

Od teraz, korzystając z metody open_file nie musimy się martwić otwartymi plikami. Zostaną automatycznie zamknięte podczas wywołania metody teardown.

Jak to RCov z Rails3 i Rubym 1.9.2 przestał działać (i jak temu zaradzić)

Po przesiadce na Rails 3.0.0 i nowego Rubiego 1.9.2, okazało się że RCov nie działa (tzn. działa ale wyniki jakie przedstawia są "lekko" niewłaściwe). Zresztą, zapuszczając RCova dostajemy taką oto wiadomość:

** WARNING: Ruby 1.9 Support is experimental at best. Don't expect correct results! **

Dodatkowo rcov mimo podawania konkretnych plików, sam zaczął testować także gemy i masę innego kodu.

Jak temu zaradzić?
Niezgodność z Rubym 1.9.2 naprawić można stosunkowo prosto. Trzeba po prostu... wrócić do Rubiego 1.8.7 :)

Dzięki skorzystaniu z RVM (Ruby Versiom Manager), jest to bardzo proste. Instalujemy RVM, zmieniamy wersję Rubiego i już.

Pozostaje jednak jeszcze jeden dziwny problem. Z jakiegoś powodu pliki nie chcą ładować się z automatu tylko trzeba każdy z nich wywołać ręcznie za pomocą polecenia:

ruby sciezka_do_pliku/plik.rb sciezka_do_plku2/plik2.rb

Ten problem rozwiązałem, pisząc sobie prosty skrypt który "zbiera" mi pliki, które mam przetestować. Następnie odpala RCova poleceniem system, podając jako parametry wszystkie pliki na których ma RCov działać. Opiszę ten kod w miarę dokładnie, a następnie pokażę sposób użycia.

Pierwszą rzeczą jaką trzeba zrobić, jest zadeklarowanie jakich bibliotek nam potrzeba, oraz zdefiniowanie tych katalogów, w których mamy nie szukać plików *.rb:

# coding: utf-8
require 'find'
require 'fileutils'

EXCLUDED = ['.svn', 'fixtures', 'additional_data', 'coverage', 'integration', 'performance']

jak więc widać, nie będziemy przeprowadzać testów integracyjnych oraz wydajnościowych. Pominiemy także repo svna i inne niepotrzebne katalogi.

Aby sprawdzić czy ścieżka którą chcemy odpytać jest ścieżką niedozwoloną, tworzymy taką metodę:

# Czy dana ścieżka nie należy do sciezek z testami
def excluded?(path)
  EXCLUDED.each{ |ex|
    return true if path.include?(ex)
  }
  return false
end

Następnie musimy stworzyć metodę, która będzie nam zwracała ilość plików *.rb w katalogu w którym jesteśmy (aby nie podawać ścieżek bez plików do RCova):

def file_nr(path)
  all = Dir.open(path).collect
  file_nr = 0
  all.each { |f|
    file_nr += 1 if !File.directory?("#{path}/#{f}") && File.exists?("#{path}/#{f}")
  }
  file_nr
end

Przyjęliśmy tutaj założenie że w katalogach z których zbieramy, nie ma plików innych niż *.rb.

Kolejna metodą której będziemy potrzebować, jest metoda przygotowująca nam z tablicy ścieżek, jednego stringa o wzorze:

katalog/*.rb katalo2/*.rb ... katalonN/*.rb

Metoda ta zbiera ścieżki i dodaje do nich *.rb:

def prepare_params(paths)
  paths.collect! { |d| d + '/*.rb'}
  params = ''
  paths.each{ |d| params += " #{d}"}
  params
end

Teraz nie pozostaje nam nic innego, jak tylko "przelecieć" katalog test/, powyciągać to co trzeba i odpalić Rcova:

helpers = []
functional = []
unit = []
# Pobierzmy listę katalogów w /test
Find.find(path = './test/'){ |f|
  if File.directory?(f) && !excluded?(f) && file_nr(f) > 0
    # Rozdzielamy testy helperow, unitow i functionals
    if f.include?('helpers')
      helpers << f
      next
    end

    if f.include?('functional')
      functional << f
      next
    end

    if f.include?('unit')
      unit << f
      next
    end

  end

}
# #{prepare_params(helpers)}
system "rcov #{prepare_params(unit)} #{prepare_params(functional)} -o ./test/coverage/ --exclude gems --exclude boot.rb"

Kod ten wrzucamy do katalogu script, a uruchamiamy wpisując:

# Zakladamy ze jestesmy w katalogu głownym aplikacji (Rails.root)
ruby script/rcov.rb

Warto pamiętać że RCov działający na 1.8.7, ale testujący kod pisany pod 1.9.2, niekoniecznie zrealizuje poprawnie wszystkie testy. Część testów (u mnie około 1%) nie zadziała. Winą za to można obarczyć chociażby to że 1.8.7 nie ma metody cover?, z której ja dość często korzystam. Nie stanowi to jednak problemu jeśli z RCova korzystamy poglądowo a do testów właściwych wykorzystujemy rake test.

Plik z gotowym kodem, do pobrania tutaj: rcov_runner

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑