Bandeiras de mobilização e botões vermelhos

É melhor trabalhar numa organização hierarquizada, top-down, ou numa mais solta, bottom-up? Depois de alguns anos, aprendi que ambos os estilos têm vantagens e desvantagens, funcionam e podem ser igualmente agradáveis.

Naturalmente, em ambos os estilos, a boa liderança depende de uma miríade de fatores. Como contribuidor individual, porém, notei duas atitudes de meus superiores que fazem uma diferença especial.

Nas organizações bottom-up é bem comum contratar pessoas competentes e deixá-las fazer sua mágica. Nesses cenários, sempre me ajudou muito o líder desenhar bem a missão. Não é definir o que vai ser feito, mas sim objetivos concretos com o qual podemos nos conectar. Sem isso, o subordinado pode se perder em tarefas irrelevantes. Já aconteceu muito comigo: dediquei muito esforço a bugs mínimos.

Isso é algo que tende a não acontecer em ambientes hierarquizados. Mesmo que nos dediquemos a algo irrelevante, a “culpa” não será nossa. Por isso mesmo, porém, frequentemente recebemos instruções francamente perigosas. Eu lembro de uma funcionalidade que me solicitaram. Eu sabia claramente como fazer — já tinha feito muitas similares — mas meu chefe me ordenou outra abordagem. Soube que, anos depois, após perdas de dados e duas reescritas, fizeram exatamente o que eu faria no primeiro dia.

Enfim, se você for dar liberdade, dê também uma missão clara, uma bandeira de mobilização que nos guie e nos inspire a tentar. Se for dar instruções rigidamente, disponibilize um botão vermelho para alertas que venham de baixo. Liderança vai além disso, mas essas duas atitudes já ajudam bastante.

Dois problemas num Jenkinsfile

Ah, as sutilezas do shell script…

Já usei vários servidores Jenkins aqui na Liferay, sempre como desenvolvedor. Na Liferay Cloud, porém, configurar e rodar Jenkins é uma tarefa rotineira. Resolvi então reforçar meu conhecimento seguindo os passos de Guided Tour to Jenkins. Foi quando aconteceu uma coisa curiosa.

O processo de build no Jenkins é definidos por arquivos Jenkinsfile. Um Jenkinsfile contém, entre outras coisas, vários estágios do processo de build. Cada estágio possui um ou mais passos (isto é, comandos a serem executados).

A sessão sobre passos múltiplos me apresentou o retry (que executa um um passo até ele ser bem-sucedido) e timeout (que limita o tempo que um passo pode tomar). Para testá-los, escrevi o seguinte no meu Jenkinsfile:

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
                      [ $((R % 3)) -eq 0 ] && sleep 10
          '''
         }
       }
    }
  }
}Code language: PHP (php)

Mesmo que você não conheça Jenkinsfiles, é possível ter uma ideia do que se passa:

  • No retry(3), coloquei um script shell que, a cada três execuções, falha. Assim, poderia ver Jenkins fazendo novas tentativas de executá-lo.
  • No passo de timeout(5, SECONDS) há um script que, a cada três execuções, demora dez segundos para terminar. Como há um tempo-limite de 5 segundos, essas execuções demoradas vão falhar.

Mandei o aquivo para GitHub configurei Jenkins para “buildar” o repositório e… todos os builds falharam! Isto não estava certo, eu esperava que o build falhasse pouco mais de um terço das vezes. O que estava acontecendo?

O problema é que, na minha cabeça, a condição do teste [ $((R % 3)) -eq 0 ] && exit 1 seria verdadeira um terço das vezes, fazendo com que o script encerrasse com status 1. (Todo status não-nulo é considerado falha.) Só que, quando a condição era falsa, o próprio teste falhava. Era o status do teste que Jenkins verificava!

Entendido o problema, foi fácil resolver: adicionei, ao final do script, um operador OU. Por quê? Porque se o teste passar, o exit 1 é executado e o script falha; se o teste não passar, porém, o sh vai executar o comando depois do ||, que sempre vai retornar zero.

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            [ $((R % 3)) -eq 0 ] && sleep 10 || echo 'slow ok'
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Funcionou! Exceto que agora o build raramente falhava, e nunca por tempo excedido. Por que será?

É que concatenei || echo 'slow ok' também na frente do passo do timeout. O script desse passo de fato levava 10 segundos para executar um terço das vezes, e Jenkins de fato cancelava o script.

O problema é que Jenkins “mata” o comando sleep 10, que então retorna um status de erro. Só que o echo 'slow ok' também “come” esse status de falha quando o build é cancelado. Por isso, mesmo quando abortado, o build aparecia como bem-sucedido. Por outro lado, eu não poderia simplesmente deixar o teste falhar, como antes.

A solução é aquela tão comum: parar de usar truquezinhos engenhosos e escrever código claro. Troquei os && e || por um condicional if. Convenhamos que até mais legível ficou, né?

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            if [ $((R % 3)) -eq 0 ] ; then 
              sleep 10
            fi
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Às vezes sinto que estamos na era CVS das ferramentas de CI, aguardando um Subversion que simplifique nossa vida. Mas confesso estar feliz que tenhamos, hoje essas ferramentas disponíveis. Por ora, vou focando em aprender com esses incidentes.

Busca textual no Liferay Portal com Elasticsearch – Parte 2: Arquitetura

Quando migramos de Lucene para Elasticsearch, Liferay manteve o mesmo processo: ao salvar uma entidade, indexamos seus campos. Só que, agora, no Elasticsearch.

Contudo, Elasticsearch não é uma biblioteca: é um sistema servidor altamente distribuído, rodando em clusters. Isto nos forçou a tomar novas decisões, e nos presenteou com algumas lições valiosas. Para entendê-las, convém revisar como Elasticsearch pode ser usado.

O modelo de programação de Elasticsearch

No Elasticsearch, os dados são salvos como pares de chave-valor, agrupados em documentos. Os documentos representam as instâncias das entidades.

Um índice contem vários documentos. Para efeitos práticos, todos os documentos de um índice são mesmo tipo (ao contrário de SGBDs, que aceitam várias tabelas em um banco de dados). O tipo define que campos são encontrados nos documentos e é usualmente dinâmico, permitindo que campos novos sejam adicionados por mera inserção.

Vários índices para vários portais

É muito comum trabalhar, no Elasticsearch, com vários índices diferentes em um mesmo cluster, para o mesmo formato de dados. Como seriam os índices do Liferay. Para responder isso, é necessário ter em mente que Liferay é multi-tenant.

Sabe quando você entra, digamos, em wordpress.com ou blogspot.com e cria um blog? Seu blog é totalmente independente de qualquer outro no mesmo site. O mesmo se aplica ao Liferay Portal, que permite criar instâncias independentes em uma mesma instalação.

Os engenheiros decidiram então que cada instância teria seu próprio índice no Elasticsearch. Separar os dados desta maneira nos permitiu controlar melhor o acesso de uma instância, assim como nos deu flexibilidade para otimizar Elasticsearch.

Por outro lado, embora houvesse vários índices, todos teriam o mesmo formato. Como seria esse formato?

Tudo junto e misturado

Seguindo as recomendações da Elastic, cada índice teria um só tipo. Neste tipo, indexamos as mais diversas entidades.

Na hora em que uma entidade é salva no banco de dados, os valores de suas colunas são indexados em um só tipo de documento. Assim, o post de blog, o artigo do CMS, o comentário no fórum… todos serão representados pelo mesmo formato. O título do blog, do evento, da wiki etc. se tornarão o campo title de vários documentos. O mesmo se passa com campos como content, summary, description

Estes campos representam características comuns a várias entidades. Quando todas elas são indexadas em um só índice, com um schema uniforme, a busca se torna mais simples: basca procurar pela string nestes documentos.

Os documentos Elasticsearch também contêm bastante metadados, sendo alguns dos mais importantes o campo entryClassName (o tipo da entidade que o documento representa, geralmente o nome de uma classe Java) e entryClassPK (identificador da entidade na tabela SQL). Desta forma, podemos encontrar o que o usuário procura e, a partir daí, recuperar a entidade correta do bando de dados.

Esta arquitetura é uma herança da busca com Lucene mas, nada surpreendentemente, funcionou perfeitamente com Elasticsearch. Esta foi uma de nossas primeiras lições:

Para implementar busca texutal, junte vários tipos de unidade em um só tipo de documento.

Decidida a arquitetura dos dados, a próxima questão a resolver era: como indexar e buscar as entidades no índice da instância. Este será o tema da parte 3 desta série.

Protected: Por que eu provavelmente não usaria Haskell em um projeto profissional

This content is password protected. To view it please enter your password below:

Busca textual no Liferay Portal com Elasticsearch – Parte 1: História

Qual a aplicação indispensável de um portal web?

Liferay Portal é uma plataforma de portais web, referência de mercado, open source e em Java. Um portal web é um software que suporta várias aplicações, chamadas portlets; portlets podem inclusive estar na mesma página web. O Liferay Portal, por exemplo, inclui CMS, blog, wiki, fóruns, calendário, formulários, workflow, e-commerce e muito mais, por padrão. Ainda é possível criar suas próprias aplicações e acrescentá-las ao portal.

Por que é difícil fazer buscas em um portal

Uma aplicação obrigatória em todo portal é a pesquisa global. Usuários querem uma barra de busca simples, que procure por dados de todas as diferentes aplicações.

Isto é desafiador em um portal baseado em SQL como Liferay. Cada aplicação salva seus dados em tabelas próprias. SQL “tradicional” não é uma solução razoável para estes casos: as queries seriam imensas e difíceis de construir.

Além disso, faltam a SQL funcionalidades básicas de busca textual. Alguns dialetos de SQL possuem capacidades extras, mas Liferay Portal pode rodar em mais de quarenta SGBDs diferentes. Não poderíamos utilizar extensões de vendors.

Outra solução era necessária.

Bibliotecas como solução

Logotipo da Lucene, em que a palavra "lucene" aparece escrita em letras minúsculas cursivas, e o "l" do começo possui algumas "franjas" apontando para trás, dando a sensação de velocidade, movimento para frente.
Logotipo de Lucene

Essa outra solução era Apache Lucene, a biblioteca Java de indexação e busca. Lucene tornou-se praticamente a escolha natural quando se trabalha com pesquisa textual. Mesmo soluções mais robustas como Solr e Elasticsearch são construídos ao redor dele.

Como utilizávamos a biblioteca? Quando o Liferay salvava uma entidade buscável no banco SQL, passava seus campos para o Lucene. Lucene indexava o conteúdo (em um formato eficiente para busca) e o salvava no disco.

Havia porém um desafio. Em produção, portais Liferay sempre rodam em clusters. Como o Lucene salvava os dados no disco local, o conteúdo era indexado apenas no nó do cluster Liferay que recebeu a requisição. Para superar esta limitação, sincronizávamos os índices de Lucene com JGroups, uma biblioteca de mensageria que enviava mensagens entre os vários nós.

Utilizamos esta solução por vários anos… até adotarmos Elasticsearch.

O que é Elasticsearch?

Logotipo atual do Elasticsearch, em que três figuras geométricas formam um "E" meio arredondado.
Logotipo atual do Elasticsearch. (Confesso que achava o antigo mais fofinho.)

Resumidamente, Elasticsearch é banco de dados NoSQL criado com ênfase em para busca textual.

Baseado em Lucene, Elasticsearch é extremamente escalável, distribuído e simples. Hoje, além de ser quase um padrão de facto para busca textual, Elasticsearch é popular para análise de dados, monitoramento e segurança, especialmente em combinação com o resto da Elastic Stack. Atualmente utilizamos para muitas destas outras funções na Liferay, mas hoje focaremos aqui na busca textual.

As razões da migração

Por que abandonamos nossa solução e adotamos Elasticsearch? Por várias razões.

  1. Lucene é uma biblioteca de baixo nível, o que tornava a manutenção do código mais trabalhosa e cara. Elasticsearch é mais poderoso, simples e flexível.
  2. Elasticsearch roda e suas próprias VMs e cluster, enquanto Lucene rodava na JVM de Liferay. Mover a busca para fora das VMs do Liferay reduziu os recursos necessários nos servidores Liferay.
  3. As JVMs separadas do Elasticsearch também permite otimizá-las para busca sem conflitos com as configurações ideais para portal das VMs do Liferay.
  4. Clusterização e sincronização já eram partes de Elasticsearch. Pudemos deletar milhares de linhas de código!

Além de tudo, Elasticsearch se tornava padrão de mercado. Foi uma escolha natural.

Neste processo, aprendemos muito sobre Elasticsearch. Veremos um pouco do que descobrimos na próxima parte.

Ajustando a Cartela de Kanban em Validação

Em Kanban, não se pode voltar uma tarefa pra fases anteriores. Como toda regra, há situações em que não se aplica. Quais? Veja nesse excelente post do grande Eduardo Zoby!

Um exemplo em especial me fez pensar.

Uma pulga atrás da orelha

O que fazer quando bugs críticos são encontrados em uma tarefa que satisfaz os critérios para revisão? Zoby explica que o time deixa a cartela em validação. Enquanto isso, subtarefas são criadas e executadas.

É uma maneira correta e funcional de modelar o processo. Como desenvolvedor, porém, sinto certo incômodo com este cenário. Por razões bobas, confesso, mas interessantes:

  1. Trabalhar em algo que está “em validação” soa impreciso. Nós estamos desenvolvendo quando corrigimos falhas.
  2. Pessoalmente, fico ansioso quando tenho de esperar pelo resultado de uma validação. Não há diferença prática, mas há uma psicológica. Temos agência sobre o trabalho que estamos desenvolvendo, mas não sobre o trabalho nosso que estão revisando.
  3. Falta clareza na posse da tarefa. Quem é o responsável, o desenvolvedor ou o validador? O tíquete define a quem está atribuído, mas a própria coluna acaba sendo dividida entre dois papeis.

Mas… como seria uma alternativa?

Imaginando uma fábrica

Sendo Kanban inspirado por processos de manufatura, talvez um retorno às origens pudesse oferecer alguma alternativa.

Me imaginei (porque não sei nada de engenharia de produção) em uma fábrica de carros. O que acontece com um automóvel em montagem se um de seus componentes se provou defeituoso? O carro não volta na esteira de produção, mas tampouco vai ser testado. Mais crucialmente, o componente também não volta para ser consertado: ele é descartado.

Não dá para mapear um processo industrial para engenharia de software, mas a ideia de descartar o componente é instigante. Descartar os “componentes” da nossa história é um bom caminho.

Não se conserta o amortecedor enquanto se testa o carro

Como seriam os “componentes” de nossa cartela. Seriam subtarefas, mas apenas se satisfizerem duas condições:

  1. são bem definidas o suficiente para serem avaliáveis;
  2. são pequenas o suficiente para que possam ser descartadas.

Durante o desenvolvimento, subtarefas seriam criadas prodigamente, inclusive e especialmente se a tarefa estiver “pronta” (naquele sentido obscuro que nós programadores damos ao termo). Uma vez terminada, cada subtarefa é enviada para validação; caso falhe, é fechada e outra subtarefa é aberta. Enquanto isso, a história continua em desenvolvimento.

Imagino várias vantagens neste processo:

  1. as alterações são revisadas e mergeadas gradativamente — bem à moda do trunk-based development.
  2. regressões seriam descobertas mais perto da mudança que as causou.
  3. estatísticas seriam provavelmente mais precisas: trabalho em bugs descobertos não seria mais computado como validação.

Certamente há pontos em aberto. O que fazer, por exemplo, se ao final a história não for aprovada? Há que se pensar. Eu tornaria a própria a revisão em uma subtarefa, mas nunca fiz isso. Como não sou engenheiro de produção, não sei o que fazem com os carros que falham ao final — talvez haja alguma inspiração la!

Um caso de uso peculiar do passado remoto

Isso tudo é um exercício mental, mas à medida que pensava, lembrei que já fiz algo similar.

Cuidei do Calendário da Liferay por anos, como uma “euquipe.” Comunicar-me com todos os stakeholders e codificar ao mesmo tempo era um desafio. Resolvi isto criando muitas subtarefas — alguns tíquetes chegaram a ter mais de trinta. Enquanto isso, os bugs e histórias permaneciam em desenvolvimento.

Às vantagens acima, somaram-se outras:

  1. os pull requests menores que eram revistos rapidamente.
  2. os stakeholders podiam ver o que eu estava fazendo, se quisessem.

Para um componente não-estratégico, estes detalhes foram fundamentais!

Vale a pena?

Deixar em validação ou em desenvolvimento a tarefa a corrigir é um detalhe semântico. Ainda assim, acho que pensar no assunto pode ser produtivo. Como não sou agilista, não tenho tanto know-how em definir workflows de times, mas o ponto de vista dos contribuidores individuais podem ser úteis. Quem sabe este aqui não é?

A Evolução da Carreira Remota

Como o profissional remoto pode crescer na sua carreira? Sendo o trabalho remoto uma revolução recente, esta é uma das questões complexas que se apresentam. No geral, os empregados de colarinho branco tendem a crescer mais quando mudam de empresa, e na minha experiência isto é até mais comum em ambientes remotos. Contudo, é possível crescer na mesma carreira como trabalhador remoto — se a empresa fizer seu dever de casa. Desde que comecei a comunidade Hipótese Remota, conheci várias destas empresas remote-first que trabalham pesado para evoluir seus colaboradores, e resolvi buscar um pouco de seu conhecimento.

Tudo azul na carreira

Convidei meu velho amigo da UnB, Fabricio Buzeto, co-founder da bxblue, uma startup fintech daqui de Brasília que cresce a olhos vistos, para um café no dia 5 de outubro de 2020. Ele me contou como funciona o plano de carreira da bxblue: “Não fazemos nada diferente das empresas presenciais. Temos avaliações periódicas e um calendário de promoção.”

No caso deles, há duas partes no plano de carreiras: um plano de cargos e salários, que reconhece como o profissional cresceu, e um plano de competências, que irá ajudá-lo ainda mais. Cada departamento tem critérios de avaliação. Por exemplo o call center da área de atendimento tem métricas de fechamentos, a engenharia de software não tem.

É importante que os critérios sejam claros, objetivos e, notavelmente, coletivos. “Na bx, a métrica é a média do time,” me contou Fabrício. “A nossa área de atendimento, hoje, é dez vezes mais produtiva que o melhor atendente do passado.” Essa estratégia, que foca na equipe e não no indivíduo, facilita encontrar os reais problemas por trás das métricas. “Se os atendentes estão convertendo pouco, que habilidade falta para converter mais? O software pode estar apresentando uma proposta inviável. Ou o atendente pode demorar a ligar, ou não consegue ligação e não busca por outros meios.”

Planejamento distribuído para carreiras distribuídas

Instigado por conhecer o plano de carreira da bxblue, resolvi conversar com outras empresas. Lembrei da minha querida amiga Karina Varela da Red Hat — lembram das dicas genias dela sobre trabalho em casa com família? Ela havia me contato como, sendo uma corporação que cresceu do free software dos anos 90 e 2000, a Red Hat sempre foi internacional, distribuída e, portanto, remote-first. Marquei outro cafezinho com ela e sua superior, Glauce Santos, gerente de aquisições da América Latinha da Red Hat, para o dia 8 de outubro de 2020. Perguntei: como é o plano de carreira na RH?

Para minha surpresa, eles não tinham!

Glauce me explicou que o desenvolvimento na Red Hat é muito mais localizado. “Nós não temos um plano de carreira, como numa Big Four. Nós temos uma cultura aberta e avaliação de desempenho individualizada, com o gestor.” Neste caso, o principal responsável é o próprio colaborador. “A responsabilidade fica na mão do funcionário” informa Glauce. Para isso, o suporte do gestor é fundamental, conforme nos conta Karina: “O gerente ajuda o colaborador a chegar onde ele quer.”

Embora seja uma abordagem bem diferente da bxblue, há uma semelhança: critérios são definidos pelas áreas e times. “O consultor é avaliado pela satisfação do cliente, ou talvez pelas horas trabalhadas. No suporte se vê quantos chamados foram atendidos, e quantos SLAs foram cumpridos. Os times de venda têm metas“, Karina me disse. Glauce complementa: “Empregados são avaliados por responsabilidades principais, goals, metas e objetivos. E há um plano de desenvolvimento para cada um, desenvolvido em conjunto com o gestor.”

Crescendo para os lados

Um dos pontos mais interessantes da conversa foi sobre algo que também é incentivado aqui na Liferay: mudar de funções e times. Eu mesmo, por exemplo, já mudei de time várias vezes. Isto ocorre tanto na bxblue quanto na Red Hat.

“Nós somos incentivados a mudar de times através de processos seletivos internos,” Karina me contou. O lado bom disso é que, quando não há vagas ou orçamento para promoções, o empregado pode se desenvolver expandindo os horizontes. Complementa Glauce: “Na RH, sempre tem oportunidade. Às vezes não temos o budget ou o ‘próximo passo’, mas sempre tem mais responsabilidades. Há carreiras verticais, horizontais e em Y, é possível mudar de área, virar especialista, etc.”

Movimentações laterais são uma solução para crescimento de carreira? Em minha opinião, podem ser uma boa ferramenta complementar, mas não substituem, naturalmente, as promoções. Tanto o colaborador quanto o RH precisam ter ciência de que isto não substitui crescimento. Por outro lado, acredito que pode ajudar bastante. Eu mesmo já resolvi, com mudanças de time ou área, problemas que acreditava necessitar de uma promoção. Ainda fui atrás da promoção, mas a mudança foi uma lufada de ar fresco.

Sumário

Hoje, até mais que à época, as empresas têm de se esforçar mais para manter seus colaboradores. Com cada vez mais empresas adotando o remote-first, o desafio é ainda maior. Planos de carreira bem definidos, como os da bxblue, são um grande diferencial para manter profissionais. Não são obrigatórios, porém, como o modelo distribuído da Red Hat provou. Mudanças de time e área também são úteis mas, pessoalmente, creio ser necessário estar atento para evitar estagnação.

E você, o que acha? Deixe seu comentário aí!

Não me Interprete Mal: Improvisando Testes para um Interpretador

Estou amando ler o livro Crafting Interpreters. Nele, Bob Nystrom nos ensina como escrever um interpretador implementando uma pequena linguagem de programação chamada Lox. Há muito tempo não me divertia tanto programando! Além de bem escrito, o livro é engraçado e ensina bem mais coisas do que eu esperava. Mas estou tendo um problema.

Os snipptes no livro são escritos para copiar e colar. Contudo, o livro tem desafios ao final de cada capítulo, estes desafios não têm código-fonte e por vezes nos forçam a alterar muito o interpretador. Eu faço todos estes exercícios, e como resultado meu interpretador é diferente demais do código mostrado no livro. Como consequência, várias vezes quebro alguma parte do interpretador.

Como resolver isso?

Testes unitários seriam frágeis, já que a estrutura do código muda frequentemente. Testes de fim a fim parecem mais práticos nesse caso. Assim, para cada nova funcionalidade da linguagem, escrevi um programinha. Por exemplo, meu interpretador deve criar clausuras, e para garantir isso copiei o programa em Lox abaixo para o arquivo counter.lox:

fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1".
counter(); // "2".

O resultado deste programa devem ser os números 1 e 2 impressos em linhas separadas. Então coloquei esses valores em um arquivo chamado counter.lox.out. O programa também não pode falhar, então criei um arquivo vazio chamado counter.lox.err. (Em alguns casos é preciso garantir que o programa Lox falhará. Nesses casos, o arquivo .lox.err deve ter conteúdo.)

Pois bem, escrevi programas e arquivos de saída para vários exemplos. Agora preciso comparar os resultados dos programas com as saídas esperadas. Resolvi então usar a ferramenta que mais me ajuda nos momentos de urgência: shell script. Fiz um script Bash com um laço for iterando sobre todos os exemplos:

for l in examples/*.lox
do

done

Para cada exemplo, executei o programa Lox, redirecionando as saídas para arquivos temporários:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err
done

Agora, comparamos a saída real com a saída esperada com diff. Quando compara dois arquivos, diff returna 0 se não há diferença, 1 se existe alguma diferença, ou 2 em caso de erro. Como em Bash o condicional if considera 0 como verdadeiro, basta checar a negação do retorno de diff.

Se o programa imprimir algo na saída-padrão diferente do que está no arquivo .lox.out, temos uma falha:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi
done

Também comparamos a saída de erro com o arquivo .lox.err:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi
done

Por fim, verifico se houve alguma falha e reporto o resultado:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Nem todos os meus programas em Lox podem ser checados, porém. Por exemplo, há um programa que cronometra a execução de loops, é impossível prever o valor que ele vai imprimir. Por isso, adicionei a possibilidade de pular alguns programas: basta criar um arquivo com a extensão .lox.skip:

for l in examples/*.lox
do
  if [ -e $l.skip ]
  then
    echo SKIP $l
    continue
  fi

  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Se, porém, tenho um exemplo em Lox e ele não possui os arquivos de saída esperada (nem o arquivo .lox.skip) então tenho um problema e o script inteiro falha:

for l in examples/*.lox
do
  if [ -e $l.skip ]
  then
    echo SKIP $l
    continue
  elif [ ! -e $l.out ] || [ ! -e $l.err ]
  then
    echo missing $l.out or $l.err
    exit 1
  fi

  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Com isto, meu script testador está pronto. Vejamos como se comporta:

$ ./lcheck.sh
PASS examples/attr.lox
PASS examples/bacon.lox
PASS examples/badfun.lox
PASS examples/badret.lox
PASS examples/bagel.lox
PASS examples/bostoncream.lox
PASS examples/cake.lox
PASS examples/checkuse.lox
PASS examples/circle2.lox
PASS examples/circle.lox
1d0
< 3 1c1 < --- > [line 1] Error at ',': Expect ')' after expression.
FAIL examples/comma.lox
PASS examples/counter.lox
PASS examples/devonshinecream.lox
PASS examples/eclair.lox
PASS examples/fibonacci2.lox
PASS examples/fibonacci.lox
PASS examples/func.lox
PASS examples/funexprstmt.lox
PASS examples/hello2.lox
PASS examples/hello3.lox
PASS examples/hello.lox
PASS examples/math.lox
PASS examples/notaclass.lox
PASS examples/noteveninaclass.lox
PASS examples/point.lox
PASS examples/retthis.lox
PASS examples/scope1.lox
PASS examples/scope.lox
PASS examples/supersuper.lox
PASS examples/thisout.lox
PASS examples/thrice.lox
SKIP examples/timeit.lox
PASS examples/twovars.lox
PASS examples/usethis.lox
PASS examples/varparam.lox

Opa, aparentemente removi o suporte ao operador vírgula por acidente. Ainda bem que fiz este script, não é?

Espero que este post tenha sido minimamente interessante! Agora, vou consertar meu operador vírgula e seguir lendo este livro maravilhoso.

Trocando figurinhas sobre o terminal

Uma das minhas diversões nesta Copa foi montar um álbum de figurinhas. Na verdade, montei o álbum porque me filho queria muito, mas me diverti também, eu acho.

Álbum de figurinhas da Copa do Mundo de 2018, aberto na página da França, com três figurinhas faltando

Infelizmente, não completei ainda

Parte importante de colecionar figurinhas é trocar as repetidas. Através de mensagens em grupos de WhatsApp, dizemos quais repetidas temos e quais figurinhas ainda precisamos. Como programador, me recusei a ficar comparando as listas, então escrevi um programinha em Python (com doctests e tudo) para encontrar intersecções.

O laptop sumido

Semana passada vieram à minha casa para trocar figurinhas. Eu tinha as listas de repetidas e necessárias, tanto minhas quanto da outra colecionadora, mas o meu script estava em um outro laptop. Eu nem sabia onde esse laptop estava, e a visita estava com pressa.

Não daria tempo de achar o computador, ou de reescrever o programa. Ou mesmo de comparar manualmente.

Hora de usar alguns comandos Unix!

O formato das listas

As listas geralmente têm esse formato:

15, 18, 26, 31, 40, 45 (2), 49, 51, 110, 115, 128, 131 (2), 143, 151, 161, 162, 183 (2), 216 (2), 221, 223, 253, 267 (3), 269, 280, 287, 296, 313, 325, 329, 333 (2), 353 (3), 355, 357, 359, 362, 365, 366, 371, 373, 384, 399, 400, 421 (2), 445, 457, 469, 470, 498 (2), 526, 536, 553, 560, 568, 570, 585, 591 (2), 604 (2), 639 (2), 660.

Basicamente, eu precisava remover tudo que não fossem dígitos, além dos números entre parênteses, e comparar duas listas. Fácil até.

Preprocessando com sed

Primeiro, preciso remover os contadores entre parênteses:

$ cat list.txt | sed 's/([^)]*)//g'
15, 18, 26, 31, [...] 591 , 604 , 639 , 660.

(Eu sei, UUOC. Que seja.)

Depois, coloco cada número em uma linha:

$ cat list.txt | sed 's/([^)]*)//g' | sed 's/, */\n/g'

Depois, limpo cada linha de qualquer caractere que não seja um dígito:

cat list.txt | sed 's/([^)]*)//g' | sed 's/, */\n/g' | sed 's/[^0-9]*\([0-9]*\)[^0-9]*/\1/g'

(Na prática chamo sed apenas uma vez, passando duas expressões. Aqui acho que fica mais claro invocar sed várias vezes.)

Por fim, ordeno os valores:

$ cat list.txt | sed 's/([^)]*)//g' | sed 's/, */\n/g' | sed 's/[^0-9]*\([0-9]*\)[^0-9]*/\1/g' | sort -n > mine-needed.txt

Faço isso com a lista de figuras necessárias, e também com a lista de figuras repetidas, conseguindo dois arquivos.

Encontrando intersecções com grep

Agora, preciso compará-los. Há muitas opções, e eu escolhi usar grep.

No caso, chamei grep passando um dos arquivos como entrada, e o outro arquivo como uma lista de padrões para casar, através da opção -f. Além disso, apenas o matching completo das linhas importa, então usaremos a flag -x. Por fim, pedi para grep comparar strings diretamente (ao invés de considerá-las expressões regulares) com a flag -F.

$ fgrep -Fxf mine-needed.txt theirs-repeated.txt
253
269
333
470
639

Pronto! Em um minuto, já sei quais figuras quero. Basta fazer o mesmo com as minhas repetidas.

Por que isto é interessante?

Para mim, hoje, estes one-liners não são grande coisa. O interessante é que, quando comecei a usar o terminal, eles seriam incríveis. Sério, olhe quantos pipes usamos para preprocessar os arquivos! E esse truque com o grep? Eu penava só para fazer uma regex que funcionasse! Na verdade, até solucionar esse problema, eu nem conhecia a opção -x.

Certa vez ajudei um amigo meu a processar um bom número de arquivos. Ele já levava mais de duas horas tentando fazer isso com Java, e resolvemos juntos em dez minutos com shell script. Ele então me falou o quanto queria saber shell script e me perguntou como aprender.

Pois bem, pequenos exemplos como estes, por simples que sejam, me ensinaram muito. É assim que se aprende: tentando resolver problemas, conhecendo comandos e opções aos poucos. E, no final, esta é uma habilidade muito valiosa.

Então, espero que esta pequena brincadeira enriqueça seu dia, também. Certamente enriqueceu o meu – queria ter pensado nela antes de gastar tanto tempo no script Python que escrevi!

Dê uma chance a Doctest

Um dos meus módulos Python preferidos é doctest. Com ele, é possível executar trechos de código inseridos em documentação. Você poderia, por exemplo, escrever algo assim no seu arquivo turorial.md

>>> f()
1

…e executar python -mdoctest tutorial.md. Se f() retornar 1, nada acontecerá. Se retornar algo diferente, porém, aparecerá uma mensagem de erro similar a esta:


É uma ferramenta impressionante, mas também é impopular. O problema é que Doctest é frequentemente utilizado de maneira inadequada. Por exemplo, é comum tentar escrever testes unitários como doctests. Grande erro!

Ainda assim, considero injusto desconsiderar o módulo devido a estes enganos. Doctest pode, e deve, ser usado para o que faz melhor: manter sua documentação viva, e até guiar seu desenvolvimento!

Deixe-me mostrar um exemplo.

Quando não se sabe o que fazer

Esses dias, estava escrevendo uma classe que alteraria um documento HTML utilizando xml.dom.minidom. Em um determinado momento, eu precisava de uma função que mapeasse classes CSS para nodes do documento. Esta seria uma função bem complicada por si só! Não sabia por onde começar.

Teoricamente, testes unitários seriam úteis aqui. Só não seriam tão práticos: esta era uma função interna, privada, um detalhe de implementação. Para testá-la, ela teria de ser exposta. Também precisaríamos de um novo arquivo para os testes. E, afinal, test cases não são tão legíveis.

Lendo a documentação do futuro

Ao invés disso, documentei a função primeiro. Escrevi um paragrafozinho informando o que ela faria. Só isso já clareou minha mente um pouco:

Given an xml.dom.minidom.Node, returns a map
from every “class” attribute to a list of nodes
with this class.

Então, pensei em como escrever a mesma coisa, mas como um exemplo de código. Na minha cabeça, esta função (que chamei de get_css_class_dict()) receberia um documento xml.dom.minidom. Então, criei um de exemplo:

Dado este exemplo, eu esperaria que a função retornasse um dicionário. Meu documento tem duas classes, “a” e “b”, e portanto o dicionário teria duas chaves. Cada chave teria uma lista dos nós que possuíssem tais classes. Algo mais ou menos assim:

Coloquei esses rascunhos na docstring de get_css_class_dict(). O resultado até agora foi essa função:

Dava para fazer algo similar com testes unitários, mas haveria muito mais código à volta, poluindo a documentação. Além disso,  a prosa graciosamente complementa o código, dando ritmo à leitura.

Executo os doctests, e o resultado é esse:

Basicamente, estou seguindo test-driven development, mas com documentação executável. Consegui, de uma vez, um exemplo legível e um teste básico.

Agora basta implementar a função! Usei recursão e, se o código não ficou o mais sucinto possível de primeira…

…ao menos funciona como esperado:

Opa, espere aí! O que foi isso?!

Quando a documentação erra

Pois bem, há um errozinho no meu doctest! O span não possui a classe b: é o div que a possui. Basta alterar a linha

[<DOM Element: span at ...>]

para

[<DOM Element: div at ...>]

e o doctest passará.

Não é uma maravilha? Descobri quase imediatamente um deslize na documentação. Mais que isto: se algum dia o comportamento da minha função mudar, o exemplo da minha docstring vai falhar! Saberei exatamente onde a documentação precisará de atualizações.

Fazendo doctests valerem a pena

Esta é a razão de ser de doctest. Nossa documentação possuía um erro sutil, e ao executá-la pudemos percebê-lo. Doctests não garantem a corretude do código; doctests garantem a corretude da documentação. É um aspecto bem compreendido do pacote, mas poucas pessoas parecem acreditar que isto valha a pena.

Eu acho que vale! Documentação tem a fama de ser um trabalho desagradável, mas não precisa ser. Assim como TDD torna testes mais empolgantes, é possível tornar documentação divertida com doctests.

Além disso, do mesmo modo que dificuldades com TDD indicam limitações de design, dificuldades em escrever doctest apontam para problemas na API. Se foi difícil escrever um exemplo conciso e claro, cercado de prosa, para sua API, ela provavelmente é complicada demais, não é?

Dê uma chance a Doctest

No final, entendo as restrições de doctests. São inadequados para testes unitários, por exemplo. Entretanto, doctest torna tão fácil e divertido documentar! Não entendo por que continua tão impopular.

Ainda assim, a maior vantagem para mim é como doctest torna o processo de desenvolvimento mais fácil. Há um tempo atrás, brinquei que deveríamos criar o DocDD (documentation-driven development):

Com Doctest, isto não é apenas uma brincadeira.