bddgenx 0.1.42 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,144 +1,162 @@
1
1
  # lib/bddgenx/pdf_exporter.rb
2
+ # encoding: utf-8
3
+ #
4
+ # Este arquivo define a classe PDFExporter, responsável por gerar documentos
5
+ # PDF a partir de arquivos .feature, formatados no estilo pretty Cucumber em
6
+ # preto e branco.
7
+ # Utiliza a gem Prawn para renderização de texto e tabelas.
8
+
2
9
  require 'prawn'
10
+ require 'prawn/table'
3
11
  require 'fileutils'
4
12
  require_relative 'fontLoader'
5
13
 
14
+ # Suprime aviso de internacionalização para fontes AFM internas
15
+ Prawn::Fonts::AFM.hide_m17n_warning = true
16
+
6
17
  module Bddgenx
18
+ # Gera documentos PDF baseados em arquivos .feature.
7
19
  class PDFExporter
8
- KEYWORD_COLORS = {
9
- 'Given' => '0000FF',
10
- 'When' => '008000',
11
- 'Then' => '800080',
12
- 'And' => '000000',
13
- 'But' => 'FF0000'
14
- }
15
-
16
- def self.exportar_todos(only_new: false)
20
+ # Gera PDFs de features, criando um para cada arquivo .feature ou apenas para
21
+ # o especificado em caminho_feature.
22
+ #
23
+ # @param caminho_feature [String, nil]
24
+ # Caminho para um arquivo .feature específico. Se nil ou vazio, gera para todos
25
+ # os arquivos em features/*.feature.
26
+ # @param only_new [Boolean]
27
+ # Se true, não sobrescreve arquivos PDF já existentes.
28
+ # @return [Hash<Symbol, Array<String>>]
29
+ # Retorna um hash com duas chaves:
30
+ # - :generated => array de caminhos dos PDFs gerados
31
+ # - :skipped => array de caminhos dos PDFs que foram pulados
32
+ def self.exportar_todos(caminho_feature: nil, only_new: false)
17
33
  FileUtils.mkdir_p('reports/pdf')
18
- generated, skipped = [], []
19
- Dir.glob('features/*.feature').each do |feature|
20
- nome = File.basename(feature, '.feature')
21
- destino = "reports/pdf/#{camel_case(nome)}.pdf"
22
- if File.exist?(destino)
34
+ features_list = if caminho_feature && !caminho_feature.empty?
35
+ [caminho_feature]
36
+ else
37
+ Dir.glob('features/*.feature')
38
+ end
39
+
40
+ generated = []
41
+ skipped = []
42
+
43
+ features_list.each do |feature|
44
+ unless File.file?(feature)
45
+ warn "⚠️ Feature não encontrada: #{feature}"
46
+ next
47
+ end
48
+
49
+ nome = File.basename(feature, '.feature')
50
+ destino = "reports/pdf/#{camel_case(nome)}.pdf"
51
+
52
+ if only_new && File.exist?(destino)
23
53
  skipped << destino
24
- else
25
- exportar_arquivo(feature, destino)
26
- generated << destino
54
+ next
27
55
  end
56
+
57
+ exportar_arquivo(feature, destino)
58
+ generated << destino
28
59
  end
60
+
29
61
  { generated: generated, skipped: skipped }
30
62
  end
31
63
 
64
+ # Converte uma string para formato camelCase, removendo caracteres especiais.
65
+ #
66
+ # @param str [String] A string de entrada a ser transformada.
67
+ # @return [String] String no formato camelCase.
32
68
  def self.camel_case(str)
33
69
  clean = str.gsub(/[^0-9A-Za-z ]/, '')
34
70
  parts = clean.split(/ |_/)
35
71
  ([parts.first&.downcase] + (parts[1..] || []).map(&:capitalize)).join
36
72
  end
37
73
 
74
+ # Gera um documento PDF a partir de um arquivo .feature, aplicando estilos
75
+ # de cabeçalhos, cenários, passos e tabelas conforme padrões Cucumber.
76
+ #
77
+ # @param origem [String] Caminho para o arquivo .feature de origem.
78
+ # @param destino [String] Caminho onde o PDF será salvo.
79
+ # @return [void]
38
80
  def self.exportar_arquivo(origem, destino)
39
81
  FileUtils.mkdir_p(File.dirname(destino))
40
82
  conteudo = File.read(origem, encoding: 'utf-8')
41
83
 
42
- families = FontLoader.families
43
- usar_ttf = !families.empty?
44
-
45
84
  Prawn::Document.generate(destino, page_size: 'A4', margin: 50) do |pdf|
46
- if usar_ttf
47
- pdf.font_families.update(families)
48
- pdf.font 'DejaVuSansMono'
49
- else
50
- pdf.font 'Courier'
51
- end
52
- pdf.font_size 10
53
-
54
- # Cabeçalho da Feature
55
- feature_title = conteudo.lines.find { |l| l =~ /^Feature:/i }&.strip || File.basename(origem)
56
- pdf.text feature_title, size: 18, style: :bold
57
- pdf.move_down 8
58
-
59
- # Descrição pós-Feature
60
- descr = []
61
- conteudo.each_line.drop_while { |l| l !~ /^Feature:/i }.drop(1).each do |l|
62
- break unless l.strip.start_with?('#')
63
- descr << l.strip.sub(/^#\s*/, '')
64
- end
65
- unless descr.empty?
66
- pdf.text descr.join("\n"), size: 11, align: :left
67
- pdf.move_down 12
68
- end
85
+ pdf.font 'Courier'
86
+ pdf.font_size 9
87
+
88
+ table_buffer = []
69
89
 
70
- examples_rows = []
71
90
  conteudo.each_line do |linha|
72
91
  text = linha.chomp
73
- next if text =~ /^#\s*language:/i
74
92
 
75
- # Coleciona linhas de tabela
76
- if text =~ /^\|.*\|$/
77
- examples_rows << text.gsub(/^\||\|$/, '').split('|').map(&:strip)
93
+ # Agrega linhas de tabela até o bloco terminar
94
+ if text =~ /^\s*\|.*\|/i
95
+ # Remove bordas laterais e separa colunas
96
+ row = text.gsub(/^\s*\||\|\s*$/, '').split('|').map(&:strip)
97
+ table_buffer << row
78
98
  next
79
- else
80
- if examples_rows.any?
81
- pdf.table(examples_rows, header: true, width: pdf.bounds.width) do
82
- self.header = true
83
- self.row_colors = ['EFEFEF', 'FFFFFF']
84
- self.cell_style = { size: 9, font: pdf.font }
85
- end
86
- pdf.move_down 6
87
- examples_rows.clear
99
+ elsif table_buffer.any?
100
+ # Renderiza tabela acumulada
101
+ pdf.table(table_buffer, header: true, width: pdf.bounds.width) do
102
+ self.header = true
103
+ self.row_colors = ['EEEEEE', 'FFFFFF']
104
+ self.cell_style = { size: 8, font: 'Courier' }
88
105
  end
106
+ pdf.move_down 4
107
+ table_buffer.clear
89
108
  end
90
109
 
91
110
  case text
92
- when /^Scenario Outline:/i, /^Scenario:/i
93
- pdf.stroke_color 'CCCCCC'
94
- pdf.stroke_horizontal_rule
95
- pdf.stroke_color '000000'
111
+ when /^\s*(Feature|Funcionalidade):/i
96
112
  pdf.move_down 6
97
113
  pdf.text text, size: 14, style: :bold
114
+ pdf.move_down 4
115
+ when /^\s*(Background):/i
116
+ pdf.text text, size: 11, style: :italic
117
+ pdf.move_down 4
118
+ when /^\s*(Scenario(?: Outline)?|Esquema do Cenário):/i
98
119
  pdf.move_down 6
99
- when /^Examples:/i
100
120
  pdf.text text, size: 12, style: :bold
101
121
  pdf.move_down 4
102
- when /^(Given|When|Then|And|But)\b/i
103
- keyword, rest = text.split(' ', 2)
104
- color = KEYWORD_COLORS[keyword] || '000000'
122
+ when /^\s*(Examples|Exemplos):/i
123
+ pdf.text text, size: 11, style: :bold
124
+ pdf.move_down 4
125
+ when /^\s*@/i
126
+ pdf.text text, size: 8, style: :italic
127
+ pdf.move_down 4
128
+ when /^(?:\s*)(Given|When|Then|And|But|Dado|Quando|Então|E|Mas)\b/i
129
+ # Passo Gherkin: destaca palavra-chave e texto
130
+ keyword, rest = text.strip.split(' ', 2)
105
131
  pdf.indent(20) do
106
132
  pdf.formatted_text [
107
- { text: keyword, styles: [:bold], color: color },
108
- { text: " " + (rest || ''), color: '000000' }
109
- ], size: 10
133
+ { text: keyword, styles: [:bold] },
134
+ { text: rest ? " #{rest}" : '' }
135
+ ], size: 9
110
136
  end
111
137
  pdf.move_down 2
112
- when /^@/ # tags
113
- pdf.formatted_text [
114
- { text: text, styles: [:italic], size: 9, color: '555555' }
115
- ]
116
- pdf.move_down 4
117
- when /^#/ # comentários
118
- pdf.text text.sub(/^#\s*/, ''), size: 9, style: :italic, color: '777777'
119
- pdf.move_down 4
120
- when ''
138
+ when /^\s*$/
121
139
  pdf.move_down 4
122
140
  else
123
141
  pdf.text text
124
142
  end
125
143
  end
126
144
 
127
- # Exibe última tabela se houver
128
- if examples_rows.any?
129
- pdf.table(examples_rows, header: true, width: pdf.bounds.width) do
130
- self.header = true
131
- self.row_colors = ['EFEFEF', 'FFFFFF']
132
- self.cell_style = { size: 9, font: pdf.font }
145
+ # Renderiza tabela remanescente, se houver
146
+ if table_buffer.any?
147
+ pdf.table(table_buffer, header: true, width: pdf.bounds.width) do
148
+ self.header = true
149
+ self.row_colors = ['EEEEEE', 'FFFFFF']
150
+ self.cell_style = { size: 8, font: 'Courier' }
133
151
  end
152
+ pdf.move_down 4
134
153
  end
135
154
 
136
- pdf.move_down 20
137
- pdf.number_pages('Página <page> de <total>', align: :right, size: 8)
155
+ # Numeração de páginas
156
+ pdf.number_pages 'Página <page> de <total>', align: :right, size: 8
138
157
  end
139
158
  rescue => e
140
159
  warn "❌ Erro ao gerar PDF de #{origem}: #{e.message}"
141
160
  end
142
161
  end
143
162
  end
144
-
@@ -1,20 +1,40 @@
1
+ # lib/bddgenx/tracer.rb
2
+ # encoding: utf-8
3
+ #
4
+ # Este arquivo define a classe Tracer, responsável por gerar e manter
5
+ # informações de rastreabilidade de cenários e passos em um arquivo CSV.
6
+ # Útil para auditoria e análise de cobertura de cenários gerados.
7
+
1
8
  require 'csv'
2
9
  require 'fileutils'
3
10
 
4
11
  module Bddgenx
12
+ # Classe para adicionar registros de rastreabilidade a um relatório CSV.
5
13
  class Tracer
14
+ # Adiciona entradas de rastreabilidade para cada passo de cada grupo
15
+ # da história em um arquivo CSV localizado em 'reports/output/rastreabilidade.csv'.
16
+ #
17
+ # @param historia [Hash]
18
+ # Objeto de história contendo :quero (título da funcionalidade) e :grupos,
19
+ # onde cada grupo possui :tipo, :tag, e :passos (Array<String>)
20
+ # @param nome_arquivo_feature [String]
21
+ # Nome do arquivo .feature de onde os passos foram gerados
22
+ # @return [void]
6
23
  def self.adicionar_entrada(historia, nome_arquivo_feature)
24
+ # Garante existência do diretório de saída
7
25
  FileUtils.mkdir_p('reports/output')
8
26
  arquivo_csv = 'reports/output/rastreabilidade.csv'
9
27
 
28
+ # Cabeçalho padrão do CSV: identifica colunas
10
29
  cabecalho = ['Funcionalidade', 'Tipo', 'Tag', 'Cenário', 'Passo', 'Origem']
11
30
 
12
31
  linhas = []
13
32
 
33
+ # Itera sobre grupos de passos para compor linhas de rastreabilidade
14
34
  historia[:grupos].each_with_index do |grupo, idx|
15
- tipo = grupo[:tipo]
16
- tag = grupo[:tag]
17
- passos = grupo[:passos]
35
+ tipo = grupo[:tipo]
36
+ tag = grupo[:tag]
37
+ passos = grupo[:passos] || []
18
38
 
19
39
  nome_funcionalidade = historia[:quero].gsub(/^Quero\s*/, '').strip
20
40
  nome_cenario = "Cenário #{idx + 1}"
@@ -31,10 +51,18 @@ module Bddgenx
31
51
  end
32
52
  end
33
53
 
54
+ # Escreve ou anexa as linhas geradas ao CSV
34
55
  escrever_csv(arquivo_csv, cabecalho, linhas)
35
56
  end
36
57
 
58
+ # Escreve ou anexa registros em um arquivo CSV, criando cabeçalho se necessário.
59
+ #
60
+ # @param caminho [String] Caminho completo para o arquivo CSV de rastreabilidade
61
+ # @param cabecalho [Array<String>] Array de títulos das colunas a serem escritos
62
+ # @param linhas [Array<Array<String>>] Dados a serem gravados no CSV (cada sub-array é uma linha)
63
+ # @return [void]
37
64
  def self.escrever_csv(caminho, cabecalho, linhas)
65
+ # Verifica se é um novo arquivo para incluir o cabeçalho
38
66
  novo_arquivo = !File.exist?(caminho)
39
67
 
40
68
  CSV.open(caminho, 'a+', col_sep: ';', force_quotes: true) do |csv|
@@ -1,32 +1,58 @@
1
+ # lib/bddgenx/validator.rb
2
+ # encoding: utf-8
3
+ #
4
+ # Este arquivo define a classe Validator, responsável por validar a estrutura
5
+ # de uma história antes de gerar cenários ou arquivos .feature.
6
+ # Verifica presença de cabeçalho obrigatório e integridade dos grupos de passos.
7
+
1
8
  module Bddgenx
9
+ # Valida objetos de história garantindo que possuam campos e blocos corretos.
2
10
  class Validator
11
+ # Valida o hash de história fornecido.
12
+ #
13
+ # Verifica:
14
+ # - Presença das chaves :como, :quero, :para no cabeçalho
15
+ # - Presença de pelo menos um grupo em :grupos
16
+ # - Cada grupo deve ter ao menos passos ou exemplos
17
+ # - Grupos do tipo "EXAMPLES" devem conter uma tabela de exemplos válida
18
+ #
19
+ # @param historia [Hash] Objeto de história com chaves :como, :quero, :para e :grupos
20
+ # @return [Boolean] Retorna true se a história for válida; caso contrário, false
3
21
  def self.validar(historia)
4
22
  erros = []
5
23
 
24
+ # Verificação do cabeçalho obrigatório
6
25
  unless historia[:como] && historia[:quero] && historia[:para]
7
26
  erros << "❌ Cabeçalho incompleto (Como, Quero, Para obrigatórios)"
8
27
  end
9
28
 
10
- if historia[:grupos].empty?
29
+ # Verificação de grupos de passos
30
+ if historia[:grupos].nil? || historia[:grupos].empty?
11
31
  erros << "❌ Nenhum grupo de blocos detectado"
12
32
  else
13
33
  historia[:grupos].each_with_index do |grupo, idx|
14
- if grupo[:passos].empty? && grupo[:exemplos].empty?
34
+ # Cada grupo deve conter passos ou exemplos
35
+ if (grupo[:passos].nil? || grupo[:passos].empty?) &&
36
+ (grupo[:exemplos].nil? || grupo[:exemplos].empty?)
15
37
  erros << "❌ Grupo #{idx + 1} do tipo [#{grupo[:tipo]}] está vazio"
16
38
  end
17
39
 
18
- if grupo[:tipo] == "EXAMPLES" && grupo[:exemplos].none? { |l| l.strip.start_with?('|') }
40
+ # Validação específica para blocos de exemplos
41
+ if grupo[:tipo].casecmp('EXAMPLES').zero? &&
42
+ grupo[:exemplos].none? { |l| l.strip.start_with?('|') }
19
43
  erros << "❌ Grupo de EXAMPLES no bloco #{idx + 1} não contém tabela válida"
20
44
  end
21
45
  end
22
46
  end
23
47
 
48
+ # Exibe erros e retorna false se houver falhas
24
49
  if erros.any?
25
50
  puts "⚠️ Erros encontrados no arquivo:"
26
51
  erros.each { |e| puts " - #{e}" }
27
52
  return false
28
53
  end
29
54
 
55
+ # História válida
30
56
  true
31
57
  end
32
58
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bddgenx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.42
4
+ version: 0.1.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-15 00:00:00.000000000 Z
11
+ date: 2025-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prawn
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: prawn-table
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: prawn-svg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
27
55
  description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
28
56
  rastreabilidade e integração com CI/CD.
29
57
  email:
@@ -49,7 +77,6 @@ files:
49
77
  - lib/bddgenx/utils/fontLoader.rb
50
78
  - lib/bddgenx/utils/parser.rb
51
79
  - lib/bddgenx/utils/pdf_exporter.rb
52
- - lib/bddgenx/utils/tipo_param.rb
53
80
  - lib/bddgenx/utils/tracer.rb
54
81
  - lib/bddgenx/utils/validator.rb
55
82
  - lib/bddgenx/version.rb
@@ -1,16 +0,0 @@
1
-
2
- module Bddgenx
3
- # Módulo para inferir o tipo de parâmetro a partir de exemplos
4
- class TipoParam
5
- # Retorna 'string', 'int', 'float' ou 'boolean'
6
- def self.determine_type(nome, estrutura)
7
- return 'string' unless estrutura[:cabecalho].include?(nome)
8
- idx = estrutura[:cabecalho].index(nome)
9
- vals = estrutura[:linhas].map { |l| l[idx] }
10
- return 'boolean' if vals.all? { |v| %w[true false].include?(v.downcase) }
11
- return 'int' if vals.all? { |v| v.match?(/^\d+$/) }
12
- return 'float' if vals.all? { |v| v.match?(/^\d+\.\d+$/) }
13
- 'string'
14
- end
15
- end
16
- end