Há um mês escrevi um post sobre configurações fora do padrão em Rails, onde descrevi como executar testes com models cujas tabelas não existem no banco de dados local, e sim em uma base externa. Porém, depois de postar, verifiquei que há um outro problema não resolvido com a configuração que descrevi nesse post: relacionamentos HABTM (has and belongs to many).

Nos relacionamentos HABTM, normalmente, há dois models, um correspondente a cada tabela do banco de dados. Como a relação entre eles é de muitos para muitos, há uma terceira tabela no banco de dados, que é responsável pela associação das demais tabelas. Como essa tabela só costuma ter dois campos, que são FK’s correspondentes às PK’s dessas tabelas, ela não precisa ter um model; basta criar o relacionamento dos dois models como has_and_belongs_to_many, passando como parâmetro join_table essa tabela intermediária.

A configuração descrita no post anterior carrega manualmente os fixtures de cada model, porém não carrega fixtures correspondentes à tabela intermediária. Para isso, precisei implementar um novo método na classe Test::Unit::TestCase (arquivo test/test_helper.rb):

def set_habtm_fixtures(class1, class2)
  return unless (class1.reflections && class1.reflections.values)
  id1 = nil
  id2 = nil
  table = nil

  # Verifica qual dos relacionamentos do model class1 está associado à tabela class2
  class1.reflections.values.each do |r|
    # Se a classe associada for class2 e for uma relação HABTM, le os FK's e o nome da tabela
    if (r.klass == class2 && !r.instance_values['options'][:join_table].nil?)
      id1 = r.primary_key_name
      id2 = r.association_foreign_key
      table = r.instance_values['options'][:join_table]
      break
    end
  end
  return if table.nil?
  connection = class1.connection

  data = File.open(File.join(RAILS_ROOT, 'test', 'fixtures', "#{table}.yml")).readlines.join
  result = ERB.new(data).result
  parsed = YAML.load(result)

  # Exclui todos os registros da tabela
  connection.execute "DELETE FROM #{table}"

  parsed.values.each do |value|
    value1 = value[id1] || 'NULL'
    value2 = value[id2] || 'NULL'
    connection.execute "INSERT INTO #{table} (#{id1}, #{id2}) values (#{value1}, #{value2})"
  end
end

Este método ficou bem “feio”, pois, como não existe um model correspondente a esta tabela, precisei criar a query manualmente. O método recebe dois nomes de classes (ActiveRecord) como parâmetro. Primeiramente é verificado qual dos relacionamentos do model class1 está associado a class2, para descobrir quais são as FK’s e o nome da tabela. Em seguida, os registros desta tabela são excluídos, e cada linha do arquivo de fixtures é carregada (usando a conexão de um dos ActiveRecords).

Além disso, modifiquei o método set_fixtures desta mesma classe, criado no post anterior, pois percebi que não era necessário passar o nome da tabela como parâmetro, basta usar o método table_name:

def set_fixtures (class_name)
  table = class_name.table_name
  return unless class_name.kind_of?(ActiveRecord::Base)

  # Define a conexao usada pela classe
  ActiveRecord::Base.connection = base.connection
  Fixtures.create_fixtures(File.join(RAILS_ROOT, 'test', 'fixtures'), table) { base.connection }
end

Para exemplificar como usar estes métodos, imagine um cadastro de usuários com grupos, onde um usuário pode fazer parte de mais de um grupo. Neste exemplo, teríamos um model Usuario (tabela usuarios), um model Grupo (tabela grupos) e uma tabela usuarios_grupos, sem um model correspondente. Na classe de teste do model Usuario, teríamos o seguinte:

class UsuarioTest < ActiveSupport::TestCase
  def setup
    set_fixtures(Usuario)
    set_fixtures(Grupo)
    set_habtm_fixtures(Usuario, Grupo)
  end

  # Testes
end

No método setup, que é executado automaticamente quando os testes são executados, as duas chamadas a set_fixtures carregam as fixtures das tabelas usuarios e grupos, respectivamente; a chamada a set_habtm_fixtures atualiza a tabela usuarios_grupos.


Os métodos attr_reader, attr_writer e attr_accessor do Ruby servem para simplificar a criação de setters e getters para atributos de instância. Ex:

class Teste
  @valor = 1
  attr_accessor :valor
end

No código acima, o método attr_accessor já cria o getter e o setter para o atributo valor:

t = Teste.new
t.valor = 10
puts t.valor #=> 10

Porém, como fazer o mesmo para atributos de classe? Eu fiz essa pergunta no forum RubyOnBr. O Shairon Toledo me respondeu com o código do método attr_static_accessor, que é, na prática, o equivalente ao attr_accessor, só que para atributos de classe. Eu complementei o código dele com os métodos attr_static_reader e attr_static_writer:

class Module
  def attr_static_reader(*args)
    args.each do |meth|
      init_var(meth)
      set_reader(meth)
    end
  end

  def attr_static_writer(*args)
    args.each do |meth|
      init_var(meth)
      set_writer(meth)
    end
  end

  def attr_static_accessor(*args)
    args.each do |meth|
      init_var(meth)
      set_reader(meth)
      set_writer(meth)
    end
  end

  private
  def init_var(var_name)
    var = "@@#{var_name}".to_sym
    self.send(:class_variable_set, var, nil) unless self.send(:class_variable_defined?, var)
  end

  def set_reader(var_name)
    self.class.send(:define_method, var_name) {
      self.send(:class_variable_get, "@@#{var_name}".to_sym)
    }
  end

  def set_writer(var_name)
    self.class.class_eval %Q{
      def #{var_name}=(value)
        self.send(:class_variable_set, "@@#{var_name}".to_sym,value)
      end
    }
  end
end

Agora é possível fazer o seguinte:

class Teste
  @@valor = 1
  attr_static_accessor :valor
end

puts Teste.valor #=> 1
Teste.valor = 10
puts Teste.valor #=> 10

Alguns programas no Ubuntu permitem a impressão para o formato PDF, como o Evince e o Gedit. Para ter essa opção disponível em qualquer programa, basta instalar uma impressora virtual de PDF, usando o pacote cups-pdf. Se ele não estiver instalado na sua distribuição, instale-o com o comando sudo apt-get install cups-pdf.

Para criar uma impressora de PDF, acesse a opção “System” -> “Administration” -> “Printing” e depois “New Printer”. Depois selecione “Print into PDF file” como tipo de impressora, e na tela seguinte, “Generic” como fabricante e “PDF file generator” como modelo. Finalmente, dê um nome para a impressora. Feito isso, a impressora de PDF ficará disponível para qualquer programa que tenha a opção de impressão. Os PDFs serão criados no diretório PDF dentro do home do usuário.

O processo acima já foi descrito diversas vezes, em vários blogs e sites sobre Linux. Porém, decidi escrever sobre isso porque a maioria dos artigos pára por aí, não informando como alterar as configurações do cups-pdf e nem como resolver alguns dos problemas mais comuns.

Configurações

O arquivo de configuração do cups-pdf fica em /etc/cups/cups-pdf.conf. Neste arquivo, ficam definidos o diretório de destino dos PDFs, o diretório de spool, as regras para formação dos nomes de arquivos a partir do nome do documento impresso, configurações de segurança e permissões, o grupo de usuários que tem permissão para usar esta impressora, tipo de log, configurações do Ghostscript e outros. Se você alterar algum destes parâmetros, será necessário executar sudo /etc/init.d/cupsys restart para que as modificações tenham efeito.

Problemas

Os problemas mais comuns com o cups-pdf são:

  • se após enviar um trabalho para a impressora o arquivo correspondente não aparecer no diretório PDF dentro do seu home (ou o diretório que você tiver definido no arquivo de configuração), verifique o log (por padrão fica em /var/log/cups/cups-pdf.log). Ele pode ajudar a descobrir o que ocorreu (ex: problemas de permissão)
  • verifique se o usuário está no grupo correspondente definido no arquivo de configuração (grupo lp por padrão)
  • se você alterar o diretório de destino dos PDFs, altere também o arquivo /etc/apparmor.d/usr.sbin.cupsd, na seção /usr/lib/cups/backend/cups-pdf. Este arquivo contém as regras de segurança do AppArmor, e define os diretórios onde o cups-pdf tem permissão de escrita. Após alterá-lo, execute sudo /etc/init.d/cupsys restart
  • se o log do cups-pdf apresentar a mensagem “[ERROR] failed to set file mode for PDF file (non fatal)”, execute o comando sudo aa-complain cupsd

A execução de testes em Rails é feita, normalmente, com o comando rake test. Este comando executa automaticamente todos os testes unitários e funcionais. Porém, algumas vezes queremos executar apenas uma parte dos testes, seja porque sabemos que outra parte do sistema está dando algum erro que pretendemos tratar depois, seja porque acabamos de alterar um trecho do código (ou dos testes) e queremos verificar se estes estão OK.

Para executar apenas os testes unitários, executamos o comando rake test:units. Para os testes funcionais, rake test:funcionals. Mas se quisermos executar um arquivo de testes específico, devemos trocar o rake pelo próprio ruby:

ruby test/unit/usuario_test.rb

Para ser ainda mais específico e executar um único método, basta acrescentar o parâmetro --name:

ruby test/unit/usuario_test.rb --name test_dados

Outra vantagem de testar executando o Ruby diretamente (sem o rake) é que não ocorre o problema de tabelas não existentes no ambiente de desenvolvimento, como ocorre com o rake.

Referência: Rails: Unit Test without Rails


Quem já tentou tratar caracteres acentuados em Ruby deve ter percebido que a linguagem não considera estes caracteres como letras. Métodos como upcase e downcase são “locale insensitive”, como diz a descrição destes métodos no manual do Ruby:

str.downcase => Returns a copy of str with all uppercase letters replaced with their lowercase counterparts. The operation is locale insensitive—only characters `A’ to `Z’ are affected.

O projeto Brazilian Rails foi criado com o objetivo de resolver este e outros problemas. O projeto é instalado como um plugin para Rails, e define novos métodos para a classe String, como o upcase_br e o downcase_br, que podem ser usados em substituição aos métodos originais. O plugin acrescenta ainda outras funcionalidades, como data, hora, feriados, dinheiro e mensagens de erro, todas adaptadas ao português brasileiro.

Ontem enviei um patch para o projeto, contribuindo com alguns novos métodos para Strings. Em breve a versão atualizada deverá ser disponibilizada, conforme o post do Celestino Gomes.