bddgenx 0.1.31 → 0.1.33

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 750091e9fa8dbfbc0456300c190a1e2632c8d562014c8792b61a74d44f5a8343
4
- data.tar.gz: f9ad48899cca0bfa88a6ef3e7ab453b10676d2d1f1fa6d7628c3435837b88e10
3
+ metadata.gz: 051fe63425b07f1d16462519caf52f72241117de91e89e7b168df58d2c1c36db
4
+ data.tar.gz: 22bf2b4478fbb808d01f165baced950bdaddfacf5c4cb4adf9ed4f36711ddc88
5
5
  SHA512:
6
- metadata.gz: 0d6163a1a6dee7883a2bcdc1afc215a1b3d9bf9fe5dd05a2f9cd23cbab4ee6ad04333ce0874e66e12ce6facc0c7703862ca1466a39661228c9ee2cf7f65087b0
7
- data.tar.gz: 33d5218b2f911d40b5898bdb8053b6ba19a91a23df93d37b4910c3699b92f309b3bb5e1de9a973066dc8f9fa8d577f428cb2892bb81a7d5ed72a7ab834fa4ddf
6
+ metadata.gz: 6ca1dfab87003578859507f6d5b95ce3ddf9fedb19d6aa64e037841c6519cf874095889f8771dd8e47391e4ffd7f0e5631123c4e9b52a596f10b11fdc0b21079
7
+ data.tar.gz: 42d083f9d7673beb1b5d3a6256d9b8beeba6fa2d170499fa70a0f040fa79451d822e4d37db41d1899050012e4b26f69723014558453f8493e27a4a25e6dd26fb
data/README.md CHANGED
@@ -57,11 +57,11 @@ Como um usuario do sistema
57
57
  Quero fazer login com sucesso
58
58
  Para acessar minha conta
59
59
 
60
- [SUCCESS]@mobile
60
+ [FAILURE]
61
61
  Quando preencho email e senha válidos
62
62
  Então vejo a tela inicial
63
63
 
64
- [SUCCESS]@regressivo
64
+ [SUCCESS]
65
65
  Quando tento logar com "<email>" e "<senha>"
66
66
  Então recebo "<resultado>"
67
67
 
@@ -177,7 +177,6 @@ namespace :bddgenx do
177
177
  puts "✅ Geração BDD concluída com sucesso!"
178
178
  end
179
179
  end
180
-
181
180
  ```
182
181
 
183
182
  👨‍💻 Autor
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.31
1
+ 0.1.33
File without changes
File without changes
File without changes
File without changes
@@ -1,73 +1,97 @@
1
1
  require 'fileutils'
2
+ require_relative 'utils/tipo_param'
2
3
 
3
4
  module Bddgenx
4
5
  class Generator
5
- TIPOS_ISTQB = {
6
- "SUCCESS" => "Teste Positivo",
7
- "FAILURE" => "Teste Negativo",
8
- "ERROR" => "Teste de Erro",
9
- "EXCEPTION" => "Teste de Exceção",
10
- "VALIDATION" => "Teste de Validação",
11
- "PERMISSION" => "Teste de Permissão",
12
- "EDGE_CASE" => "Teste de Limite",
13
- "PERFORMANCE" => "Teste de Desempenho"
14
- }.freeze
6
+ GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
7
+ GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
8
+ GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
9
+ GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
10
+ ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
11
+
12
+ # Divide raw example blocks into a single table
13
+ def self.dividir_examples(raw)
14
+ raw.select { |l| l.strip.start_with?('|') }
15
+ end
16
+
17
+ # Gera .feature a partir de hash ou arquivo de história
18
+ # Respeita idioma em historia[:idioma]
19
+ def self.gerar_feature(input, override_path = nil)
20
+ historia = input.is_a?(String) ? Parser.ler_historia(input) : input
21
+ idioma = historia[:idioma] || 'pt'
22
+ nome_base = historia[:quero].gsub(/[^a-z0-9]/i, '_')
23
+ .downcase.split('_',3)[0,3].join('_')
24
+ caminho = override_path.is_a?(String) ? override_path : "features/#{nome_base}.feature"
15
25
 
16
- def self.gerar_feature(historia)
17
- idioma = historia[:idioma]
18
26
  palavras = {
19
- contexto: idioma == 'en' ? 'Background' : 'Contexto',
20
- cenario: idioma == 'en' ? 'Scenario' : 'Cenário',
21
- esquema: idioma == 'en' ? 'Scenario Outline' : 'Esquema do Cenário',
22
- exemplos: idioma == 'en' ? 'Examples' : 'Exemplos',
23
- regra: idioma == 'en' ? 'Rule' : 'Regra'
27
+ feature: idioma=='en' ? 'Feature' : 'Funcionalidade',
28
+ contexto: idioma=='en' ? 'Background' : 'Contexto',
29
+ cenario: idioma=='en' ? 'Scenario' : 'Cenário',
30
+ esquema: idioma=='en' ? 'Scenario Outline' : 'Esquema do Cenário',
31
+ exemplos: idioma=='en' ? 'Examples' : 'Exemplos',
32
+ regra: idioma=='en' ? 'Rule' : 'Regra'
24
33
  }
25
34
 
26
- frase_quero = historia[:quero].sub(/^\s*quero\s*/i, '')
27
- partes = frase_quero.split(/\s+/)[0,3] # pega só as 3 primeiras palavras
28
- slug = partes.join('_')
29
- .gsub(/[^a-z0-9_]/i, '') # remove caracteres especiais
30
- .downcase
31
- nome_base = slug
32
- caminho = "features/#{nome_base}.feature"
33
-
34
- conteudo = <<~GHERKIN
35
+ conteudo = <<~GHK
35
36
  # language: #{idioma}
36
- Funcionalidade: #{historia[:quero].sub(/^Quero\s*/, '')}
37
+ #{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
38
+ # #{historia[:como]}
39
+ # #{historia[:quero]}
40
+ # #{historia[:para]}
37
41
 
38
- #{historia[:como]}
39
- #{historia[:quero]}
40
- #{historia[:para]}
42
+ GHK
41
43
 
42
- GHERKIN
44
+ pt_map = GHERKIN_MAP_PT_EN
45
+ en_map = GHERKIN_MAP_EN_PT
46
+ detect = ALL_KEYS
43
47
 
44
48
  historia[:grupos].each_with_index do |grupo, idx|
45
- tipo = grupo[:tipo]
46
- tag = grupo[:tag]
47
- passos = grupo[:passos]
48
- exemplos = grupo[:exemplos]
49
-
49
+ passos = grupo[:passos] || []
50
+ exemplos = grupo[:exemplos] || []
50
51
  next if passos.empty?
51
52
 
52
- linha_tag = ["@#{tipo.downcase}", ("@#{tag}" if tag)].compact.join(' ')
53
- possui_parametros = passos.any? { |p| p.include?('<') } && exemplos.any?
53
+ tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
54
+
55
+ if exemplos.any?
56
+ # Scenario Outline
57
+ conteudo << " #{tag_line}\n"
58
+ conteudo << " #{palavras[:esquema]}: Exemplo #{idx+1}\n"
54
59
 
55
- if possui_parametros
56
- conteudo << " #{linha_tag}\n"
57
- conteudo << " #{palavras[:esquema]}: Exemplo #{idx + 1}\n"
58
- passos.each { |p| conteudo << " #{p}\n" }
60
+ # Renderiza cada passo, mapeando connector mesmo se não for Gherkin padrão
61
+ passos.each do |p|
62
+ line = p.strip
63
+ parts = line.split(' ', 2)
64
+ # match connector case-insensitive
65
+ con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
66
+ text = parts[1] || ''
67
+ out_conn = idioma=='en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
68
+ conteudo << " #{out_conn} #{text}
69
+ "
70
+ end
71
+
72
+ # Monta tabela de exemplos completa
73
+ # Renderiza o bloco de Examples exatamente como veio no TXT
59
74
  conteudo << "\n #{palavras[:exemplos]}:\n"
60
- exemplos.each { |linha| conteudo << " #{linha}\n" }
75
+ exemplos.select { |l| l.strip.start_with?('|') }.each do |line|
76
+ # Remove aspas apenas das células, mantendo todas as colunas e valores originais
77
+ cleaned = line.strip.gsub(/^"|"$/, '')
78
+ conteudo << " #{cleaned}\n"
79
+ end
61
80
  conteudo << "\n"
62
81
  else
63
- nome_teste = TIPOS_ISTQB[tipo] || palavras[:cenario]
64
- contexto = passos.first.gsub(/^(Dado|Quando|Então|E|Mas)\s+/, '').strip
65
- resultado = passos.last .gsub(/^(Dado|Quando|Então|E|Mas)\s+/, '').strip
66
- nome_ceno = "#{nome_teste} - #{contexto} - #{resultado}"
67
-
68
- conteudo << " #{linha_tag}\n"
69
- conteudo << " #{palavras[:cenario]}: #{nome_ceno}\n"
70
- passos.each { |p| conteudo << " #{p}\n" }
82
+ # Scenario simples
83
+ conteudo << " #{tag_line}\n"
84
+ conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
85
+ passos.each do |p|
86
+ line = p.strip
87
+ parts = line.split(' ', 2)
88
+ # match connector case-insensitive
89
+ con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
90
+ text = parts[1] || ''
91
+ out_conn = idioma=='en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
92
+ conteudo << " #{out_conn} #{text}
93
+ "
94
+ end
71
95
  conteudo << "\n"
72
96
  end
73
97
  end
@@ -79,7 +103,6 @@ module Bddgenx
79
103
  FileUtils.mkdir_p(File.dirname(caminho))
80
104
  File.write(caminho, conteudo)
81
105
  puts "✅ Arquivo .feature gerado: #{caminho}"
82
- true
83
106
  end
84
107
  end
85
- end
108
+ end
@@ -1,47 +1,38 @@
1
1
  module Bddgenx
2
+ # Tipos de blocos GUARDADOS, incluindo português EXEMPLO/EXEMPLOS
2
3
  TIPOS_BLOCOS = %w[
3
4
  CONTEXT SUCCESS FAILURE ERROR EXCEPTION
4
5
  VALIDATION PERMISSION EDGE_CASE PERFORMANCE
5
- EXAMPLES REGRA RULE
6
+ EXEMPLO EXEMPLOS RULE
6
7
  ].freeze
7
8
 
8
9
  class Parser
10
+ # Lê arquivo de história (.txt) e retorna hash com atributos e grupos
9
11
  def self.ler_historia(caminho_arquivo)
10
- linhas = File.readlines(caminho_arquivo, encoding: 'utf-8')
11
- .map(&:strip)
12
- .reject(&:empty?)
12
+ # todas as linhas, mantendo vazias
13
+ linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:rstrip)
13
14
 
14
- # Detecta idioma (padrão pt, usa en se encontrar '# lang: en')
15
- idioma = linhas.first.downcase.include?('# lang: en') ? 'en' : 'pt'
16
- linhas.shift if linhas.first.downcase.start_with?('# lang:')
17
-
18
- # Cabeçalho Gherkin (case-insensitive): Como, Eu Como ou As a
19
- como = nil
20
- linhas.each_with_index do |l, i|
21
- if l =~ /^\s*(?:como|eu como|as a)\b/i || l =~ /^\s*(?:COMO|EU COMO|AS A)\b/i
22
- como = l
23
- linhas = linhas[(i+1)..]
24
- break
25
- end
15
+ # Detecta idioma: aceita "# lang: en" ou "# language: en"
16
+ idioma = 'pt'
17
+ if linhas.first =~ /^#\s*lang(?:uage)?\s*:\s*(\w+)/i
18
+ idioma = Regexp.last_match(1).downcase
19
+ linhas.shift
26
20
  end
27
21
 
28
- # 'Quero' ou 'Eu Quero'
29
- quero = nil
30
- linhas.each_with_index do |l, i|
31
- if l =~ /^\s*(?:quero|eu quero|quero que)\b/i || l =~ /^\s*(?:QUERO|EU QUERO|QUERO QUE)\b/i
22
+ # Extrai Cabeçalho Como/Quero/Para
23
+ como, quero, para = nil, nil, nil
24
+ linhas.reject! do |l|
25
+ if l =~ /^\s*(?:como|eu como|as a)/i && como.nil?
26
+ como = l
27
+ true
28
+ elsif l =~ /^\s*(?:quero|eu quero|quero que)/i && quero.nil?
32
29
  quero = l
33
- linhas = linhas[(i+1)..]
34
- break
35
- end
36
- end
37
-
38
- # 'Para', 'Para Eu' ou 'Para Que'
39
- para = nil
40
- linhas.each_with_index do |l, i|
41
- if l =~ /^\s*(?:para|para eu|para que)\b/i || l =~ /^\s*(?:PRA|PARA EU|PARA QUE)\b/i
30
+ true
31
+ elsif l =~ /^\s*(?:para|para eu|para que)/i && para.nil?
42
32
  para = l
43
- linhas = linhas[(i+1)..]
44
- break
33
+ true
34
+ else
35
+ false
45
36
  end
46
37
  end
47
38
 
@@ -49,30 +40,30 @@ module Bddgenx
49
40
  exemplos_mode = false
50
41
 
51
42
  linhas.each do |linha|
52
- # Início de bloco de exemplos
53
- if linha =~ /^\[EXAMPLES\](?:@(\w+))?$/i
43
+ # Início de bloco de exemplos: [EXEMPLO] ou [EXEMPLOS] ou [EXAMPLES]
44
+ if linha =~ /^\[(?:EXEMPLO|EXEMPLOS|EXAMPLES)\](?:@(\w+))?$/i
54
45
  exemplos_mode = true
46
+ # inicia array se for o primeiro exemplo
55
47
  historia[:grupos].last[:exemplos] = []
56
48
  next
57
49
  end
58
50
 
59
- # Início de bloco de tipo (SUCCESS, FAILURE etc.)
51
+ # Início de bloco de tipo (SUCCESS, FAILURE, REGRA etc.)
60
52
  if linha =~ /^\[(#{TIPOS_BLOCOS.join('|')})\](?:@(\w+))?$/i
61
53
  exemplos_mode = false
62
- tipo = $1.upcase
63
- tag = $2
54
+ tipo = Regexp.last_match(1).upcase
55
+ tag = Regexp.last_match(2)
64
56
  historia[:grupos] << { tipo: tipo, tag: tag, passos: [], exemplos: [] }
65
57
  next
66
58
  end
67
59
 
68
- # Atribui linhas ao último grupo existente
60
+ # Atribui linhas ao último grupo
69
61
  next if historia[:grupos].empty?
70
- atual = historia[:grupos].last
71
-
62
+ bloco = historia[:grupos].last
72
63
  if exemplos_mode
73
- atual[:exemplos] << linha
64
+ bloco[:exemplos] << linha
74
65
  else
75
- atual[:passos] << linha
66
+ bloco[:passos] << linha
76
67
  end
77
68
  end
78
69
 
@@ -3,24 +3,28 @@ require 'fileutils'
3
3
 
4
4
  module Bddgenx
5
5
  class PDFExporter
6
- def self.exportar_todos
6
+ def self.exportar_todos(only_new: false)
7
7
  FileUtils.mkdir_p('reports/pdf')
8
+ generated = []
9
+ skipped = []
8
10
 
9
- Dir.glob('features/*.feature').each do |feature_file|
10
- base = File.basename(feature_file, '.feature')
11
- nome_pdf = camel_case(base)
12
- destino = "reports/pdf/#{nome_pdf}.pdf"
11
+ Dir.glob('features/*.feature').each do |feature|
12
+ nome = File.basename(feature, '.feature')
13
+ destino = "reports/pdf/#{camel_case(nome)}.pdf"
13
14
 
14
15
  if File.exist?(destino)
15
- puts "⚠️ PDF já existe: #{destino}"
16
+ skipped << destino
16
17
  next
17
18
  end
18
19
 
19
- exportar_arquivo(feature_file, destino)
20
- puts "📄 PDF gerado: #{destino}"
20
+ exportar_arquivo(feature, destino)
21
+ generated << destino
21
22
  end
23
+
24
+ return { generated: generated, skipped: skipped }
22
25
  end
23
26
 
27
+
24
28
  # Converte string para camelCase, removendo caracteres especiais
25
29
  def self.camel_case(str)
26
30
  # Remove tudo que não for letra ou número ou espaço
@@ -0,0 +1,115 @@
1
+ # lib/bddgenx/cli.rb
2
+ require 'fileutils'
3
+ require_relative 'parser'
4
+ require_relative 'generator'
5
+ require_relative 'steps_generator'
6
+ require_relative 'validator'
7
+ require_relative 'backup'
8
+ require_relative 'pdf_exporter'
9
+
10
+ module Bddgenx
11
+ class Runner
12
+ # Retorna lista de arquivos .txt em input/ ou só aqueles baseados em ARGV
13
+ def self.selecionar_arquivos_txt(input_dir)
14
+ ARGV.map do |arg|
15
+ nome = arg.end_with?('.txt') ? arg : "#{arg}.txt"
16
+ path = File.join(input_dir, nome)
17
+ unless File.exist?(path)
18
+ warn "⚠️ Arquivo não encontrado: #{path}"
19
+ next
20
+ end
21
+ path
22
+ end.compact
23
+ end
24
+
25
+ # Interativo: permite ao usuário escolher entre os .txt em input/
26
+ def self.choose_input(input_dir)
27
+ files = Dir.glob(File.join(input_dir, '*.txt'))
28
+ if files.empty?
29
+ warn "❌ Não há arquivos .txt no diretório #{input_dir}"
30
+ exit 1
31
+ end
32
+
33
+ puts "Selecione o arquivo de história para processar:"
34
+ files.each_with_index do |f, i|
35
+ puts "#{i+1}. #{File.basename(f)}"
36
+ end
37
+ print "Digite o número correspondente (ou ENTER para todos): "
38
+ choice = STDIN.gets.chomp
39
+
40
+ return files if choice.empty?
41
+
42
+ idx = choice.to_i - 1
43
+ unless idx.between?(0, files.size - 1)
44
+ warn "❌ Escolha inválida."
45
+ exit 1
46
+ end
47
+ [files[idx]]
48
+ end
49
+
50
+ def self.execute
51
+ history_dir = 'input'
52
+ Dir.mkdir(history_dir) unless Dir.exist?(history_dir)
53
+
54
+ # Determina quais arquivos processar
55
+ arquivos = if ARGV.any?
56
+ selecionar_arquivos_txt(history_dir)
57
+ else
58
+ choose_input(history_dir)
59
+ end
60
+
61
+ if arquivos.empty?
62
+ warn "❌ Nenhum arquivo de história para processar."
63
+ exit 1
64
+ end
65
+
66
+ # Contadores e coleções
67
+ total = features = steps = ignored = 0
68
+ skipped_steps = []
69
+ generated_pdfs = []
70
+ skipped_pdfs = []
71
+
72
+ arquivos.each do |arquivo|
73
+ total += 1
74
+ puts "\n🔍 Processando: #{arquivo}"
75
+
76
+ historia = Parser.ler_historia(arquivo)
77
+ unless Validator.validar(historia)
78
+ ignored += 1
79
+ puts "❌ Arquivo inválido: #{arquivo}"
80
+ next
81
+ end
82
+
83
+ # Geração de feature
84
+ feature_path, feature_content = Bddgenx::Generator.gerar_feature(historia)
85
+ Backup.salvar_versao_antiga(feature_path)
86
+ if Bddgenx::Generator.salvar_feature(feature_path, feature_content)
87
+ features += 1
88
+ end
89
+
90
+ # Geração de steps
91
+ if StepsGenerator.gerar_passos(feature_path)
92
+ steps += 1
93
+ else
94
+ skipped_steps << feature_path
95
+ end
96
+
97
+ # Exportação de PDFs
98
+ FileUtils.mkdir_p('reports')
99
+ results = PDFExporter.exportar_todos(only_new: true)
100
+ generated_pdfs.concat(results[:generated])
101
+ skipped_pdfs.concat(results[:skipped])
102
+ end
103
+
104
+ # Resumo final
105
+ puts "\n✅ Processamento concluído"
106
+ puts "- Total de histórias: #{total}"
107
+ puts "- Features geradas: #{features}"
108
+ puts "- Steps gerados: #{steps}"
109
+ puts "- Steps ignorados: #{skipped_steps.size}"
110
+ puts "- PDFs gerados: #{generated_pdfs.size}"
111
+ puts "- PDFs já existentes: #{skipped_pdfs.size}"
112
+ puts "- Arquivos ignorados: #{ignored}"
113
+ end
114
+ end
115
+ end
@@ -1,138 +1,108 @@
1
1
  require 'fileutils'
2
- require_relative 'utils/verificador'
2
+ require_relative 'utils/tipo_param'
3
+ require 'strscan' # para usar ::StringScanner
4
+
5
+ # lib/bddgenx/steps_generator.rb
3
6
 
4
7
  module Bddgenx
5
8
  class StepsGenerator
6
- PADROES = {
7
- 'pt' => %w[Dado Quando Então E Mas],
8
- 'en' => %w[Given When Then And But]
9
- }
10
-
11
- # Gera step definitions a partir da estrutura historia[:grupos]
12
- def self.gerar_passos(historia, nome_arquivo_feature)
13
- idioma = historia[:idioma] || 'pt'
14
- conectores = PADROES[idioma]
15
- passos_gerados = []
16
-
17
- historia[:grupos].each do |grupo|
18
- tipo = grupo[:tipo]
19
- passos = grupo[:passos]
20
- exemplos_brutos = grupo[:exemplos]
21
- exemplos = exemplos_brutos&.any? ? dividir_examples(exemplos_brutos) : nil
22
-
23
- next unless passos.is_a?(Array) && passos.any?
24
-
25
- passos.each do |linha|
26
- conector = conectores.find { |c| linha.strip.start_with?(c) }
27
- next unless conector
28
-
29
- corpo = linha.strip.sub(/^#{conector}\s*/, '')
30
- corpo_sanitizado = corpo.gsub(/"(<[^>]+>)"/, '\1')
31
-
32
- # Identifica grupo de exemplo compatível
33
- grupo_exemplo_compat = nil
34
- if exemplos
35
- exemplos.each do |tabela|
36
- cabecalho = tabela.first.gsub('|', '').split.map(&:strip)
37
- if cabecalho.any? { |col| corpo.include?("<#{col}>") }
38
- linhas = tabela[1..].map { |l| l.gsub('|', '').split.map(&:strip) }
39
- grupo_exemplo_compat = { cabecalho: cabecalho, linhas: linhas }
40
- break
41
- end
42
- end
43
- end
9
+ GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
10
+ GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
11
+ ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
12
+
13
+ # Converte texto para camelCase (para nomes de argumentos)
14
+ def self.camelize(str)
15
+ parts = str.strip.split(/[^a-zA-Z0-9]+/)
16
+ parts.map.with_index { |w, i| i.zero? ? w.downcase : w.capitalize }.join
17
+ end
44
18
 
45
- # Detecta parâmetros e gera corpo parametrizado
46
- nomes_param = corpo.scan(/<([^>]+)>/).flatten.map(&:strip)
47
- if nomes_param.any?
48
- corpo_param = corpo_sanitizado.dup
49
- nomes_param.each do |nome|
50
- tipo_param = grupo_exemplo_compat ? detectar_tipo_param(nome, grupo_exemplo_compat) : 'string'
51
- corpo_param.gsub!(/<\s*#{Regexp.escape(nome)}\s*>/, "{#{tipo_param}}")
52
- end
53
- args_list = nomes_param.map { |p| p.gsub(/\s+/, '_') }.join(', ')
54
- pending_msg = corpo
19
+ # Gera step definitions a partir de um arquivo .feature
20
+ # - "<nome>" => {string}
21
+ # - <nome> => {int}
22
+ # - "texto" => {string}
23
+ # - numeros inteiros ou floats soltos => {int}
24
+ # Respeita idioma de entrada (pt/en) para keywords geradas
25
+ def self.gerar_passos(feature_path)
26
+ raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
27
+
28
+ lines = File.readlines(feature_path)
29
+ # Detecta idioma no cabeçalho: "# language: pt" ou "# language: en"
30
+ lang = if (m = lines.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
31
+ m[/^#\s*language:\s*(\w+)/i, 1].downcase
32
+ else
33
+ 'pt'
34
+ end
35
+
36
+ pt_to_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
37
+ en_to_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
38
+
39
+ # Seleciona apenas linhas de passo
40
+ step_lines = lines.map(&:strip).select do |l|
41
+ ALL_KEYS.any? { |k| l.start_with?(k + ' ') }
42
+ end
43
+ return false if step_lines.empty?
44
+
45
+ dir = File.join(File.dirname(feature_path), 'steps')
46
+ FileUtils.mkdir_p(dir)
47
+ file = File.join(dir, "#{File.basename(feature_path, '.feature')}_steps.rb")
48
+
49
+ content = +"# encoding: utf-8\n# Auto-generated step definitions for #{File.basename(feature_path)}\n\n"
50
+
51
+ step_lines.each do |line|
52
+ # Extrai keyword original e resto do passo
53
+ orig_kw, rest = line.split(' ', 2)
54
+ # Converte keyword conforme idioma de entrada
55
+ kw = case lang
56
+ when 'en' then pt_to_en[orig_kw] || orig_kw
57
+ else en_to_pt[orig_kw] || orig_kw
58
+ end
59
+ raw = rest.dup
60
+
61
+ scanner = ::StringScanner.new(rest)
62
+ pattern = ''
63
+ tokens = []
64
+
65
+ until scanner.eos?
66
+ if scanner.check(/"<([^>]+)>"/)
67
+ scanner.scan(/"<([^>]+)>"/)
68
+ tokens << scanner[1]
69
+ pattern << '{string}'
70
+ elsif scanner.check(/<([^>]+)>/)
71
+ scanner.scan(/<([^>]+)>/)
72
+ tokens << scanner[1]
73
+ pattern << '{int}'
74
+ elsif scanner.check(/"([^"<>]+)"/)
75
+ scanner.scan(/"([^"<>]+)"/)
76
+ tokens << scanner[1]
77
+ pattern << '{string}'
78
+ elsif scanner.check(/\d+(?:\.\d+)?/)
79
+ num = scanner.scan(/\d+(?:\.\d+)?/)
80
+ tokens << num
81
+ pattern << '{int}'
55
82
  else
56
- corpo_param = corpo
57
- args_list = ''
58
- pending_msg = corpo
83
+ pattern << scanner.getch
59
84
  end
60
-
61
- passos_gerados << {
62
- conector: conector,
63
- raw: pending_msg,
64
- param: corpo_param,
65
- args: args_list,
66
- tipo: tipo
67
- } unless passos_gerados.any? { |p| p[:param] == corpo_param }
68
85
  end
69
- end
70
86
 
71
- if passos_gerados.empty?
72
- puts "⚠️ Nenhum passo detectado em: #{nome_arquivo_feature} (arquivo não gerado)"
73
- return false
74
- end
75
-
76
- nome_base = File.basename(nome_arquivo_feature, '.feature')
77
-
78
- # Define caminho de saída: prioriza pasta steps dentro de features/<nome>
79
- feature_dir = File.dirname(nome_arquivo_feature)
80
- feature_steps_dir = File.join(feature_dir, 'steps')
81
- if Dir.exist?(feature_steps_dir)
82
- FileUtils.mkdir_p(feature_steps_dir)
83
- caminho = File.join(feature_steps_dir, "#{nome_base}_steps.rb")
84
- else
85
- FileUtils.mkdir_p('steps')
86
- caminho = "steps/#{nome_base}_steps.rb"
87
- end
87
+ # Escapa aspas no padrão final
88
+ safe_pattern = pattern.gsub('"', '\\"')
89
+ signature = "#{kw}(\"#{safe_pattern}\")"
88
90
 
89
- comentario = "# Step definitions para #{File.basename(nome_arquivo_feature)}"
90
- comentario += idioma == 'en' ? " (English)" : " (Português)"
91
- conteudo = "#{comentario}\n\n"
92
-
93
- passos_gerados.each do |passo|
94
- conteudo += <<~STEP
95
- #{passo[:conector]}('#{passo[:param]}') do#{passo[:args].empty? ? '' : " |#{passo[:args]}|"}
96
- pending 'Implementar passo: #{passo[:raw]}'
97
- end
91
+ if tokens.any?
92
+ args = tokens.each_index.map { |i| "args#{i+1}" }.join(', ')
93
+ signature += " do |#{args}|"
94
+ else
95
+ signature += ' do'
96
+ end
98
97
 
99
- STEP
98
+ content << signature << "\n"
99
+ content << " pending 'Implementar passo: #{raw}'\n"
100
+ content << "end\n\n"
100
101
  end
101
102
 
102
- if Bddgenx::Verificador.gerar_arquivo_se_novo(caminho, conteudo)
103
- puts "✅ Step definitions gerados: #{caminho}"
104
- else
105
- puts "⏭️ Steps mantidos: #{caminho}"
106
- end
103
+ File.write(file, content)
104
+ puts "✅ Steps gerados: #{file}"
107
105
  true
108
106
  end
109
-
110
- def self.detectar_tipo_param(nome_coluna, exemplos)
111
- return 'string' unless exemplos[:cabecalho].include?(nome_coluna)
112
-
113
- idx = exemplos[:cabecalho].index(nome_coluna)
114
- valores = exemplos[:linhas].map { |l| l[idx].to_s.strip }
115
-
116
- return 'boolean' if valores.all? { |v| %w[true false].include?(v.downcase) }
117
- return 'int' if valores.all? { |v| v.match?(/^\d+$/) }
118
- return 'float' if valores.all? { |v| v.match?(/^\d+\.\d+$/) }
119
-
120
- 'string'
121
- end
122
-
123
- def self.dividir_examples(tabela_bruta)
124
- grupos = []
125
- grupo = []
126
- tabela_bruta.each do |linha|
127
- if linha.strip =~ /^\|.*\|$/ && grupo.any? && linha.strip == linha.strip.squeeze(' ')
128
- grupos << grupo
129
- grupo = [linha]
130
- else
131
- grupo << linha
132
- end
133
- end
134
- grupos << grupo unless grupo.empty?
135
- grupos
136
- end
137
107
  end
138
- end
108
+ end
@@ -0,0 +1,16 @@
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
data/lib/bddgenx.rb CHANGED
@@ -1,49 +1,3 @@
1
- require_relative 'bddgenx/parser'
2
- require_relative 'bddgenx/validator'
3
- require_relative 'bddgenx/generator'
4
- require_relative 'bddgenx/steps_generator'
5
- require_relative 'bddgenx/tracer'
6
- require_relative 'bddgenx/backup'
7
- require_relative 'bddgenx/cli'
8
- require_relative 'bddgenx/pdf_exporter'
9
- require_relative 'bddgenx/utils/verificador'
1
+ require_relative 'bddgenx/runner'
10
2
 
11
-
12
- cont_total = 0
13
- cont_features = 0
14
- cont_steps = 0
15
- cont_ignorados = 0
16
-
17
- # Exibe menu inicial e pergunta quais arquivos processar
18
- arquivos = Bddgenx::Cli.selecionar_arquivos_txt('input')
19
-
20
- arquivos.each do |arquivo_path|
21
- puts "\n🔍 Processando: #{arquivo_path}"
22
-
23
- historia = Bddgenx::Parser.ler_historia(arquivo_path)
24
-
25
- unless Bddgenx::Validator.validar(historia)
26
- cont_ignorados += 1
27
- puts "❌ Arquivo inválido: #{arquivo_path}"
28
- next
29
- end
30
-
31
- nome_feature, conteudo_feature = Bddgenx::Generator.gerar_feature(historia)
32
-
33
- Bddgenx::Backup.salvar_versao_antiga(nome_feature)
34
- cont_features += 1 if Bddgenx::Generator.salvar_feature(nome_feature, conteudo_feature)
35
- cont_steps += 1 if Bddgenx::StepsGenerator.gerar_passos(historia, nome_feature)
36
-
37
- # cria pasta reports raiz
38
- FileUtils.mkdir_p('reports')
39
- Bddgenx::Tracer.adicionar_entrada(historia, nome_feature)
40
- Bddgenx::PDFExporter.exportar_todos
41
- end
42
- puts "\n✅ Processamento finalizado. Arquivos gerados em: features/, steps/, output/"
43
- puts "🔄 Versões antigas salvas em: reports/backup/"
44
-
45
- puts "\n✅ Processamento finalizado:"
46
- puts "- Arquivos processados: #{cont_total}"
47
- puts "- Features geradas: #{cont_features}"
48
- puts "- Steps gerados: #{cont_steps}"
49
- puts "- Ignorados: #{cont_ignorados}"
3
+ Bddgenx::Runner.execute
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.31
4
+ version: 0.1.33
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-12 00:00:00.000000000 Z
11
+ date: 2025-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prawn
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jira-ruby
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'
27
41
  description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
28
42
  rastreabilidade e integração com CI/CD.
29
43
  email:
@@ -36,6 +50,10 @@ files:
36
50
  - README.md
37
51
  - Rakefile
38
52
  - VERSION
53
+ - assets/fonts/DejaVuSansMono-Bold.ttf
54
+ - assets/fonts/DejaVuSansMono-BoldOblique.ttf
55
+ - assets/fonts/DejaVuSansMono-Oblique.ttf
56
+ - assets/fonts/DejaVuSansMono.ttf
39
57
  - bin/bddgenx
40
58
  - lib/bddgenx.rb
41
59
  - lib/bddgenx/assets/fonts/DejaVuSansMono-Bold.ttf
@@ -43,15 +61,13 @@ files:
43
61
  - lib/bddgenx/assets/fonts/DejaVuSansMono-Oblique.ttf
44
62
  - lib/bddgenx/assets/fonts/DejaVuSansMono.ttf
45
63
  - lib/bddgenx/backup.rb
46
- - lib/bddgenx/cli.rb
47
64
  - lib/bddgenx/generator.rb
48
- - lib/bddgenx/integrations/jira.rb
49
- - lib/bddgenx/integrations/testlink.rb
50
65
  - lib/bddgenx/parser.rb
51
66
  - lib/bddgenx/pdf_exporter.rb
67
+ - lib/bddgenx/runner.rb
52
68
  - lib/bddgenx/steps_generator.rb
53
69
  - lib/bddgenx/tracer.rb
54
- - lib/bddgenx/utils/verificador.rb
70
+ - lib/bddgenx/utils/tipo_param.rb
55
71
  - lib/bddgenx/validator.rb
56
72
  - lib/bddgenx/version.rb
57
73
  homepage: https://github.com/David-Nascimento/bdd-generation
data/lib/bddgenx/cli.rb DELETED
@@ -1,36 +0,0 @@
1
- module Bddgenx
2
- class Cli
3
- def self.selecionar_arquivos_txt(diretorio)
4
- arquivos = Dir.glob("#{diretorio}/*.txt")
5
-
6
- if arquivos.empty?
7
- puts "❌ Nenhum arquivo .txt encontrado no diretório '#{diretorio}'"
8
- exit
9
- end
10
-
11
- arquivos
12
-
13
- puts "📂 Arquivos disponíveis em '#{diretorio}':"
14
- arquivos.each_with_index do |arquivo, i|
15
- puts " #{i + 1}. #{File.basename(arquivo)}"
16
- end
17
-
18
- print "\nDigite os números dos arquivos que deseja processar (ex: 1,2,3 ou 'todos'): "
19
- entrada = gets.chomp
20
-
21
- selecionados = if entrada.downcase == 'todos'
22
- arquivos
23
- else
24
- indices = entrada.split(',').map { |n| n.strip.to_i - 1 }
25
- indices.map { |i| arquivos[i] }.compact
26
- end
27
-
28
- if selecionados.empty?
29
- puts "❌ Nenhum arquivo válido selecionado."
30
- exit
31
- end
32
-
33
- selecionados
34
- end
35
- end
36
- end
@@ -1,32 +0,0 @@
1
- require 'jira-ruby'
2
-
3
- module Bddgenx
4
- module Integrations
5
- class Jira
6
- def initialize(options = {})
7
- @client = JIRA::Client.new(
8
- username: options[:username],
9
- password: options[:api_token],
10
- site: options[:site],
11
- context_path: '',
12
- auth_type: :basic
13
- )
14
- @project_key = options[:project_key]
15
- end
16
-
17
- def enviar_cenario(titulo, descricao)
18
- issue = {
19
- fields: {
20
- project: { key: @project_key },
21
- summary: titulo,
22
- description: descricao,
23
- issuetype: { name: "Task" }
24
- }
25
- }
26
-
27
- @client.Issue.build.save(issue)
28
- puts "✅ Cenário enviado para Jira: #{titulo}"
29
- end
30
- end
31
- end
32
- end
@@ -1,35 +0,0 @@
1
- require 'xmlrpc/client'
2
-
3
- module Bddgenx
4
- class TestLink
5
- def initialize(api_key, url)
6
- @server = XMLRPC::Client.new2(url)
7
- @key = api_key
8
- end
9
-
10
- def criar_caso_teste(plan_id, titulo, passos)
11
- steps_formated = passos.map.with_index(1) do |step, i|
12
- {
13
- step_number: i,
14
- actions: step,
15
- expected_results: '',
16
- execution_type: 1
17
- }
18
- end
19
-
20
- params = {
21
- devKey: @key,
22
- testprojectid: 1,
23
- testsuiteid: plan_id,
24
- testcasename: titulo,
25
- steps: steps_formated
26
- }
27
-
28
- response = @server.call('tl.createTestCase', params)
29
- puts "✅ Teste enviado ao TestLink: #{titulo}"
30
- response
31
- rescue => e
32
- puts "❌ Erro ao criar caso no TestLink: #{e.message}"
33
- end
34
- end
35
- end
@@ -1,20 +0,0 @@
1
- require 'fileutils'
2
-
3
- module Bddgenx
4
- module Verificador
5
- # Impede sobrescrita de arquivos existentes
6
- def self.gerar_arquivo_se_novo(caminho, novo_conteudo)
7
- if File.exist?(caminho)
8
- conteudo_atual = File.read(caminho, encoding: 'utf-8').strip
9
- return false if conteudo_atual == novo_conteudo.strip
10
-
11
- puts "⚠️ Arquivo já existe: #{caminho} — não será sobrescrito."
12
- return false
13
- end
14
-
15
- FileUtils.mkdir_p(File.dirname(caminho))
16
- File.write(caminho, novo_conteudo)
17
- true
18
- end
19
- end
20
- end