bddgenx 0.1.32 → 0.1.34
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 +4 -4
- data/README.md +2 -5
- data/VERSION +1 -1
- data/assets/fonts/DejaVuSansMono-Bold.ttf +0 -0
- data/assets/fonts/DejaVuSansMono-BoldOblique.ttf +0 -0
- data/assets/fonts/DejaVuSansMono-Oblique.ttf +0 -0
- data/assets/fonts/DejaVuSansMono.ttf +0 -0
- data/lib/bddgenx/generator.rb +76 -53
- data/lib/bddgenx/parser.rb +32 -41
- data/lib/bddgenx/runner.rb +115 -0
- data/lib/bddgenx/steps_generator.rb +92 -122
- data/lib/bddgenx/utils/tipo_param.rb +16 -0
- data/lib/bddgenx.rb +2 -58
- metadata +8 -18
- data/lib/bddgenx/cli.rb +0 -48
- data/lib/bddgenx/utils/verificador.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9bf7bc9f9f2860f8e305530e4e445ecdc67a9f45bfc4d6dca751f954bea45931
|
4
|
+
data.tar.gz: cdff8e55fea97da8d77fce3fde6d01d53de653a2bbaa15ce5af84bb27d58f700
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 830ff28e5997763916c6ff02cd51d5c2eb20cdb8933c2f6a695391f39a327a6c1ad330e578855e8048afc00eb5d96006d4435f56e36459ce32d41624a6fa2301
|
7
|
+
data.tar.gz: e365e0c9f7edbb717538b26bbee7dce27b9621f2fb54b26db8fde1d3996636ec4c408f9b0f50f1b2d5d81b3c8c59b494454d75516b8a0def0f6b198c7b065cba
|
data/README.md
CHANGED
@@ -18,8 +18,6 @@ bddgenx/
|
|
18
18
|
│ └── pdf/ # relatórios camelCase
|
19
19
|
├── lib/
|
20
20
|
│ ├── bddgenx/
|
21
|
-
│ │ ├── integrations
|
22
|
-
│ │ │ └── jira.rb
|
23
21
|
│ │ ├── parser.rb
|
24
22
|
│ │ ├── validator.rb
|
25
23
|
│ │ ├── generator.rb
|
@@ -59,11 +57,11 @@ Como um usuario do sistema
|
|
59
57
|
Quero fazer login com sucesso
|
60
58
|
Para acessar minha conta
|
61
59
|
|
62
|
-
[
|
60
|
+
[FAILURE]
|
63
61
|
Quando preencho email e senha válidos
|
64
62
|
Então vejo a tela inicial
|
65
63
|
|
66
|
-
[SUCCESS]
|
64
|
+
[SUCCESS]
|
67
65
|
Quando tento logar com "<email>" e "<senha>"
|
68
66
|
Então recebo "<resultado>"
|
69
67
|
|
@@ -179,7 +177,6 @@ namespace :bddgenx do
|
|
179
177
|
puts "✅ Geração BDD concluída com sucesso!"
|
180
178
|
end
|
181
179
|
end
|
182
|
-
|
183
180
|
```
|
184
181
|
|
185
182
|
👨💻 Autor
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.34
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/lib/bddgenx/generator.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
37
|
+
#{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
|
38
|
+
# #{historia[:como]}
|
39
|
+
# #{historia[:quero]}
|
40
|
+
# #{historia[:para]}
|
37
41
|
|
38
|
-
|
39
|
-
#{historia[:quero]}
|
40
|
-
#{historia[:para]}
|
42
|
+
GHK
|
41
43
|
|
42
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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.
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
data/lib/bddgenx/parser.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
11
|
-
|
12
|
-
.reject(&:empty?)
|
12
|
+
# Lê todas as linhas, mantendo vazias
|
13
|
+
linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:rstrip)
|
13
14
|
|
14
|
-
# Detecta idioma
|
15
|
-
idioma =
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
#
|
29
|
-
quero = nil
|
30
|
-
linhas.
|
31
|
-
if l =~ /^\s*(?:
|
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
|
-
|
34
|
-
|
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
|
-
|
44
|
-
|
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 =
|
63
|
-
tag =
|
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
|
60
|
+
# Atribui linhas ao último grupo
|
69
61
|
next if historia[:grupos].empty?
|
70
|
-
|
71
|
-
|
62
|
+
bloco = historia[:grupos].last
|
72
63
|
if exemplos_mode
|
73
|
-
|
64
|
+
bloco[:exemplos] << linha
|
74
65
|
else
|
75
|
-
|
66
|
+
bloco[:passos] << linha
|
76
67
|
end
|
77
68
|
end
|
78
69
|
|
@@ -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/
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
98
|
+
content << signature << "\n"
|
99
|
+
content << " pending 'Implementar passo: #{raw}'\n"
|
100
|
+
content << "end\n\n"
|
100
101
|
end
|
101
102
|
|
102
|
-
|
103
|
-
|
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,59 +1,3 @@
|
|
1
|
-
require_relative 'bddgenx/
|
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_features = 0
|
13
|
-
cont_steps = 0
|
14
|
-
cont_ignorados = 0
|
15
|
-
|
16
|
-
# Seleciona arquivos .txt
|
17
|
-
arquivos = Bddgenx::Cli.selecionar_arquivos_txt('input')
|
18
|
-
# Antes do loop
|
19
|
-
skipped_steps = []
|
20
|
-
skipped_pdfs = []
|
21
|
-
generated_pdfs = []
|
22
|
-
arquivos.each do |arquivo_path|
|
23
|
-
cont_total += 1
|
24
|
-
puts "\n🔍 Processando: #{arquivo_path}"
|
25
|
-
|
26
|
-
historia = Bddgenx::Parser.ler_historia(arquivo_path)
|
27
|
-
unless Bddgenx::Validator.validar(historia)
|
28
|
-
cont_ignorados += 1
|
29
|
-
puts "❌ Arquivo inválido: #{arquivo_path}"
|
30
|
-
next
|
31
|
-
end
|
32
|
-
|
33
|
-
# Gera feature e steps
|
34
|
-
nome_feature, conteudo_feature = Bddgenx::Generator.gerar_feature(historia)
|
35
|
-
Bddgenx::Backup.salvar_versao_antiga(nome_feature)
|
36
|
-
cont_features += 1 if Bddgenx::Generator.salvar_feature(nome_feature, conteudo_feature)
|
37
|
-
cont_steps += 1 if Bddgenx::StepsGenerator.gerar_passos(historia, nome_feature)
|
38
|
-
|
39
|
-
# Rastreabilidade, PDF
|
40
|
-
FileUtils.mkdir_p('reports')
|
41
|
-
# steps
|
42
|
-
if Bddgenx::StepsGenerator.gerar_passos(historia, nome_feature)
|
43
|
-
cont_steps += 1
|
44
|
-
else
|
45
|
-
skipped_steps << nome_feature
|
46
|
-
end
|
47
|
-
# substituir chamada direta por:
|
48
|
-
results = Bddgenx::PDFExporter.exportar_todos(only_new: true)
|
49
|
-
# exportar_todos agora fornece [:generated, :skipped]
|
50
|
-
generated_pdfs.concat(results[:generated])
|
51
|
-
skipped_pdfs.concat(results[:skipped])
|
52
|
-
end
|
53
|
-
|
54
|
-
puts "\n✅ Processamento finalizado."
|
55
|
-
puts "- Features geradas: #{cont_features}"
|
56
|
-
puts "- Steps gerados: #{cont_steps}"
|
57
|
-
puts "- Steps mantidos: #{skipped_steps.size}"
|
58
|
-
puts "- PDFs gerados: #{generated_pdfs.size}"
|
59
|
-
puts "- PDFs já existentes: #{skipped_pdfs.size}"
|
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.
|
4
|
+
version: 0.1.34
|
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-
|
11
|
+
date: 2025-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: prawn
|
@@ -24,20 +24,6 @@ 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'
|
41
27
|
description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
|
42
28
|
rastreabilidade e integração com CI/CD.
|
43
29
|
email:
|
@@ -50,6 +36,10 @@ files:
|
|
50
36
|
- README.md
|
51
37
|
- Rakefile
|
52
38
|
- VERSION
|
39
|
+
- assets/fonts/DejaVuSansMono-Bold.ttf
|
40
|
+
- assets/fonts/DejaVuSansMono-BoldOblique.ttf
|
41
|
+
- assets/fonts/DejaVuSansMono-Oblique.ttf
|
42
|
+
- assets/fonts/DejaVuSansMono.ttf
|
53
43
|
- bin/bddgenx
|
54
44
|
- lib/bddgenx.rb
|
55
45
|
- lib/bddgenx/assets/fonts/DejaVuSansMono-Bold.ttf
|
@@ -57,13 +47,13 @@ files:
|
|
57
47
|
- lib/bddgenx/assets/fonts/DejaVuSansMono-Oblique.ttf
|
58
48
|
- lib/bddgenx/assets/fonts/DejaVuSansMono.ttf
|
59
49
|
- lib/bddgenx/backup.rb
|
60
|
-
- lib/bddgenx/cli.rb
|
61
50
|
- lib/bddgenx/generator.rb
|
62
51
|
- lib/bddgenx/parser.rb
|
63
52
|
- lib/bddgenx/pdf_exporter.rb
|
53
|
+
- lib/bddgenx/runner.rb
|
64
54
|
- lib/bddgenx/steps_generator.rb
|
65
55
|
- lib/bddgenx/tracer.rb
|
66
|
-
- lib/bddgenx/utils/
|
56
|
+
- lib/bddgenx/utils/tipo_param.rb
|
67
57
|
- lib/bddgenx/validator.rb
|
68
58
|
- lib/bddgenx/version.rb
|
69
59
|
homepage: https://github.com/David-Nascimento/bdd-generation
|
data/lib/bddgenx/cli.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
module Bddgenx
|
2
|
-
class Cli
|
3
|
-
def self.confirm(message)
|
4
|
-
print "#{message} "
|
5
|
-
answer = $stdin.gets.to_s.strip.downcase
|
6
|
-
%w[s sim y yes].include?(answer)
|
7
|
-
end
|
8
|
-
|
9
|
-
# Exibe uma mensagem de pergunta e retorna a string digitada pelo usuário
|
10
|
-
def self.ask(message)
|
11
|
-
print "#{message} "
|
12
|
-
$stdin.gets.to_s.strip
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.selecionar_arquivos_txt(diretorio)
|
16
|
-
arquivos = Dir.glob("#{diretorio}/*.txt")
|
17
|
-
|
18
|
-
if arquivos.empty?
|
19
|
-
puts "❌ Nenhum arquivo .txt encontrado no diretório '#{diretorio}'"
|
20
|
-
exit
|
21
|
-
end
|
22
|
-
|
23
|
-
arquivos
|
24
|
-
|
25
|
-
puts "📂 Arquivos disponíveis em '#{diretorio}':"
|
26
|
-
arquivos.each_with_index do |arquivo, i|
|
27
|
-
puts " #{i + 1}. #{File.basename(arquivo)}"
|
28
|
-
end
|
29
|
-
|
30
|
-
print "\nDigite os números dos arquivos que deseja processar (ex: 1,2,3 ou 'todos'): "
|
31
|
-
entrada = gets.chomp
|
32
|
-
|
33
|
-
selecionados = if entrada.downcase == 'todos'
|
34
|
-
arquivos
|
35
|
-
else
|
36
|
-
indices = entrada.split(',').map { |n| n.strip.to_i - 1 }
|
37
|
-
indices.map { |i| arquivos[i] }.compact
|
38
|
-
end
|
39
|
-
|
40
|
-
if selecionados.empty?
|
41
|
-
puts "❌ Nenhum arquivo válido selecionado."
|
42
|
-
exit
|
43
|
-
end
|
44
|
-
|
45
|
-
selecionados
|
46
|
-
end
|
47
|
-
end
|
48
|
-
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
|