bddgenx 0.1.49 → 1.0.0
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/VERSION +1 -1
- data/lib/bddgenx/generator.rb +78 -82
- data/lib/bddgenx/ia/gemini_cliente.rb +120 -0
- data/lib/bddgenx/ia/gemini_generator.rb +127 -0
- data/lib/bddgenx/runner.rb +37 -2
- data/lib/bddgenx/steps_generator.rb +11 -35
- data/lib/bddgenx/utils/gherkin_cleaner.rb +57 -0
- data/lib/bddgenx/utils/parser.rb +5 -1
- data/lib/bddgenx/utils/pdf_exporter.rb +1 -1
- data/lib/bddgenx/utils/remover_steps_duplicados.rb +44 -0
- metadata +13 -9
- /data/lib/bddgenx/utils/{fontLoader.rb → font_loader.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 691d4d259476c68894a8859cc74ab4d926fdfe3b7304f5a020f05843442f33d2
|
4
|
+
data.tar.gz: 0aeff404aee94ac4d8419dec2772379a0816fdb5ce69a1e8cf967d47499e2f48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c08188d0e944723b8e109063d2c2e682e335be6c05ff731f51962daba3a3dccac072201107fb23dda9f0d0b3901f75b97a10e1bcb289cf3305ad6deab8ccc198
|
7
|
+
data.tar.gz: fd8bf60a0b13bb05e63486daa3b4ccead3ddfdc4653046e5f48b252eede5cb8af35498d0d259a23870c203a5912a63a1c70cfc01fdd9b1e5141e58bc56d4c986
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/bddgenx/generator.rb
CHANGED
@@ -7,67 +7,68 @@
|
|
7
7
|
# e esquemas de cenário com exemplos.
|
8
8
|
|
9
9
|
require 'fileutils'
|
10
|
+
require 'set'
|
11
|
+
require_relative 'utils/gherkin_cleaner'
|
12
|
+
require_relative 'ia/gemini_cliente'
|
13
|
+
require_relative 'utils/remover_steps_duplicados'
|
14
|
+
# require_relative 'clients/chatgpt_cliente'
|
10
15
|
|
11
16
|
module Bddgenx
|
12
|
-
# Gera cenários e arquivos .feature baseados em histórias e grupos de passos.
|
13
17
|
class Generator
|
14
|
-
# Palavras-chave Gherkin em Português
|
15
|
-
# @return [Array<String>]
|
16
18
|
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
17
|
-
|
18
|
-
# Palavras-chave Gherkin em Inglês
|
19
|
-
# @return [Array<String>]
|
20
19
|
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
21
|
-
|
22
|
-
# Mapeamento PT -> EN
|
23
|
-
# @return [Hash{String=>String}]
|
24
20
|
GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
25
|
-
|
26
|
-
# Mapeamento EN -> PT
|
27
|
-
# @return [Hash{String=>String}]
|
28
21
|
GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
29
|
-
|
30
|
-
# Conjunto de todas as palavras-chave suportadas (PT + EN)
|
31
|
-
# @return [Array<String>]
|
32
22
|
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
33
23
|
|
34
|
-
# Seleciona apenas as linhas que representam exemplos do cenário
|
35
|
-
#
|
36
|
-
# @param raw [Array<String>] Lista de linhas brutas do bloco de exemplos
|
37
|
-
# @return [Array<String>] Linhas que começam com '|' representando a tabela de exemplos
|
38
24
|
def self.dividir_examples(raw)
|
39
25
|
raw.select { |l| l.strip.start_with?('|') }
|
40
26
|
end
|
41
27
|
|
42
|
-
# Gera conteúdo de um arquivo .feature a partir de um hash de história ou caminho para arquivo
|
43
|
-
#
|
44
|
-
# @param input [Hash, String]
|
45
|
-
# Objeto de história com chaves :idioma, :quero, :como, :para, :grupos
|
46
|
-
# Ou caminho para um arquivo de história que será lido via Parser.ler_historia
|
47
|
-
# @param override_path [String, nil]
|
48
|
-
# Caminho de saída alternativo para o arquivo .feature
|
49
|
-
# @raise [ArgumentError] Se Parser.ler_historia lançar erro ao ler arquivo
|
50
|
-
# @return [Array(String, String)] Array com caminho e conteúdo gerado
|
51
28
|
def self.gerar_feature(input, override_path = nil)
|
52
|
-
|
53
|
-
|
29
|
+
modo = ENV['BDD_MODE']&.to_sym || :static
|
30
|
+
|
31
|
+
# IA: Lê o arquivo txt e passa para IA → aplica limpeza
|
32
|
+
if input.is_a?(String) && input.end_with?('.txt') && [:gemini, :chatgpt].include?(modo)
|
33
|
+
raw_txt = File.read(input)
|
34
|
+
historia = {
|
35
|
+
idioma: 'pt',
|
36
|
+
quero: File.basename(input, '.txt').tr('_', ' ').capitalize,
|
37
|
+
como: '',
|
38
|
+
para: '',
|
39
|
+
grupos: []
|
40
|
+
}
|
41
|
+
|
42
|
+
texto_gerado = if modo == :gemini
|
43
|
+
GeminiCliente.gerar_cenarios(raw_txt)
|
44
|
+
else
|
45
|
+
ChatGPTCliente.gerar_cenarios(raw_txt)
|
46
|
+
end
|
47
|
+
|
48
|
+
historia[:grupos] << {
|
49
|
+
tipo: 'gerado',
|
50
|
+
tag: 'ia',
|
51
|
+
passos: GherkinCleaner.limpar(texto_gerado).lines.map(&:strip).reject(&:empty?)
|
52
|
+
}
|
53
|
+
|
54
|
+
else
|
55
|
+
# Caminho tradicional (hash de entrada ou parser de arquivo)
|
56
|
+
historia = input.is_a?(String) ? Parser.ler_historia(input) : input
|
57
|
+
end
|
58
|
+
|
59
|
+
idioma = historia[:idioma] || 'pt'
|
54
60
|
cont = 1
|
55
61
|
|
56
|
-
# Geração do nome base do arquivo
|
57
62
|
nome_base = historia[:quero]
|
58
63
|
.gsub(/[^a-z0-9]/i, '_')
|
59
64
|
.downcase
|
60
|
-
.split('_'
|
61
|
-
.
|
65
|
+
.split('_')
|
66
|
+
.reject(&:empty?)
|
67
|
+
.first(5)
|
62
68
|
.join('_')
|
63
69
|
|
64
|
-
caminho =
|
65
|
-
override_path
|
66
|
-
else
|
67
|
-
"features/#{nome_base}.feature"
|
68
|
-
end
|
70
|
+
caminho = override_path || "features/#{nome_base}.feature"
|
69
71
|
|
70
|
-
# Definição das palavras-chave Gherkin conforme idioma
|
71
72
|
palavras = {
|
72
73
|
feature: idioma == 'en' ? 'Feature' : 'Funcionalidade',
|
73
74
|
contexto: idioma == 'en' ? 'Background' : 'Contexto',
|
@@ -77,75 +78,70 @@ module Bddgenx
|
|
77
78
|
regra: idioma == 'en' ? 'Rule' : 'Regra'
|
78
79
|
}
|
79
80
|
|
80
|
-
# Cabeçalho do arquivo .feature
|
81
81
|
conteudo = <<~GHK
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
82
|
+
# language: #{idioma}
|
83
|
+
#{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
|
84
|
+
# #{historia[:como]}
|
85
|
+
# #{historia[:quero]}
|
86
|
+
# #{historia[:para]}
|
87
87
|
|
88
88
|
GHK
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
90
|
+
# Set para rastrear steps únicos
|
91
|
+
passos_unicos = Set.new
|
92
|
+
pt_map = GHERKIN_MAP_PT_EN
|
93
|
+
en_map = GHERKIN_MAP_EN_PT
|
94
|
+
detect = ALL_KEYS
|
95
|
+
cont = 1
|
93
96
|
|
94
97
|
historia[:grupos].each do |grupo|
|
95
98
|
passos = grupo[:passos] || []
|
96
99
|
exemplos = grupo[:exemplos] || []
|
97
100
|
next if passos.empty?
|
98
101
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
.compact.join(' ')
|
102
|
+
tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
|
103
|
+
|
104
|
+
conteudo << " #{tag_line}\n"
|
103
105
|
|
104
106
|
if exemplos.any?
|
105
|
-
#
|
106
|
-
conteudo << " #{tag_line}\n"
|
107
|
-
conteudo << " #{palavras[:esquema]}: Exemplo #{cont} \n"
|
107
|
+
conteudo << " #{palavras[:esquema]}: Exemplo #{cont}\n"
|
108
108
|
cont += 1
|
109
|
+
else
|
110
|
+
conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
|
111
|
+
end
|
109
112
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
113
|
+
passos.each do |p|
|
114
|
+
parts = p.strip.split(' ', 2)
|
115
|
+
con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
|
116
|
+
text = parts[1] || ''
|
117
|
+
out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
|
118
|
+
linha_step = " #{out_conn} #{text}"
|
119
|
+
|
120
|
+
next if passos_unicos.include?(linha_step)
|
121
|
+
|
122
|
+
passos_unicos << linha_step
|
123
|
+
conteudo << "#{linha_step}\n"
|
124
|
+
end
|
118
125
|
|
119
|
-
|
126
|
+
if exemplos.any?
|
120
127
|
conteudo << "\n #{palavras[:exemplos]}:\n"
|
121
128
|
exemplos.select { |l| l.strip.start_with?('|') }.each do |line|
|
122
129
|
cleaned = line.strip.gsub(/^"|"$/, '')
|
123
130
|
conteudo << " #{cleaned}\n"
|
124
131
|
end
|
125
|
-
conteudo << "\n"
|
126
|
-
else
|
127
|
-
# Cenário simples
|
128
|
-
conteudo << " #{tag_line}\n"
|
129
|
-
conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
|
130
|
-
passos.each do |p|
|
131
|
-
parts = p.strip.split(' ', 2)
|
132
|
-
con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
|
133
|
-
text = parts[1] || ''
|
134
|
-
out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
|
135
|
-
conteudo << " #{out_conn} #{text}\n"
|
136
|
-
end
|
137
|
-
conteudo << "\n"
|
138
132
|
end
|
133
|
+
|
134
|
+
conteudo << "\n"
|
139
135
|
end
|
140
136
|
|
141
137
|
[caminho, conteudo]
|
142
138
|
end
|
143
139
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
140
|
+
def self.path_para_feature(arquivo_txt)
|
141
|
+
nome = File.basename(arquivo_txt, '.txt')
|
142
|
+
File.join('features', "#{nome}.feature")
|
143
|
+
end
|
144
|
+
|
149
145
|
def self.salvar_feature(caminho, conteudo)
|
150
146
|
FileUtils.mkdir_p(File.dirname(caminho))
|
151
147
|
File.write(caminho, conteudo)
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# lib/bddgenx/ia/gemini_cliente.rb
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require_relative '../utils/gherkin_cleaner'
|
6
|
+
require_relative '../utils/remover_steps_duplicados'
|
7
|
+
|
8
|
+
module Bddgenx
|
9
|
+
module IA
|
10
|
+
class GeminiCliente
|
11
|
+
GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'.freeze
|
12
|
+
|
13
|
+
def self.gerar_cenarios(historia, idioma = 'pt')
|
14
|
+
api_key = ENV['GEMINI_API_KEY']
|
15
|
+
|
16
|
+
keywords_pt = {
|
17
|
+
feature: "Funcionalidade",
|
18
|
+
scenario: "Cenário",
|
19
|
+
scenario_outline: "Esquema do Cenário",
|
20
|
+
examples: "Exemplos",
|
21
|
+
given: "Dado",
|
22
|
+
when: "Quando",
|
23
|
+
then: "Então",
|
24
|
+
and: "E"
|
25
|
+
}
|
26
|
+
|
27
|
+
keywords_en = {
|
28
|
+
feature: "Feature",
|
29
|
+
scenario: "Scenario",
|
30
|
+
scenario_outline: "Scenario Outline",
|
31
|
+
examples: "Examples",
|
32
|
+
given: "Given",
|
33
|
+
when: "When",
|
34
|
+
then: "Then",
|
35
|
+
and: "And"
|
36
|
+
}
|
37
|
+
|
38
|
+
keywords = idioma == 'en' ? keywords_en : keywords_pt
|
39
|
+
|
40
|
+
# Prompt base para solicitar saída Gherkin estruturada da IA
|
41
|
+
prompt_base = <<~PROMPT
|
42
|
+
Gere cenários BDD no formato Gherkin, usando as palavras-chave de estrutura no idioma "#{idioma}":
|
43
|
+
Feature: #{keywords[:feature]}
|
44
|
+
Scenario: #{keywords[:scenario]}
|
45
|
+
Scenario Outline: #{keywords[:scenario_outline]}
|
46
|
+
Examples: #{keywords[:examples]}
|
47
|
+
Given: #{keywords[:given]}
|
48
|
+
When: #{keywords[:when]}
|
49
|
+
Then: #{keywords[:then]}
|
50
|
+
And: #{keywords[:and]}
|
51
|
+
|
52
|
+
Atenção: Os textos e descrições dos cenários e passos devem ser escritos em português, mesmo que as palavras-chave estejam em inglês.
|
53
|
+
|
54
|
+
História:
|
55
|
+
#{historia}
|
56
|
+
PROMPT
|
57
|
+
unless api_key
|
58
|
+
warn "❌ API Key do Gemini não encontrada no .env (GEMINI_API_KEY)"
|
59
|
+
return nil
|
60
|
+
end
|
61
|
+
|
62
|
+
uri = URI("#{GEMINI_API_URL}?key=#{api_key}")
|
63
|
+
prompt = prompt_base % { historia: historia }
|
64
|
+
|
65
|
+
request_body = {
|
66
|
+
contents: [
|
67
|
+
{
|
68
|
+
role: "user",
|
69
|
+
parts: [{ text: prompt }]
|
70
|
+
}
|
71
|
+
]
|
72
|
+
}
|
73
|
+
|
74
|
+
response = Net::HTTP.post(uri, request_body.to_json, { "Content-Type" => "application/json" })
|
75
|
+
|
76
|
+
if response.is_a?(Net::HTTPSuccess)
|
77
|
+
json = JSON.parse(response.body)
|
78
|
+
|
79
|
+
unless json["candidates"]&.is_a?(Array) && json["candidates"].any?
|
80
|
+
warn "❌ Resposta da IA sem candidatos válidos:"
|
81
|
+
warn JSON.pretty_generate(json)
|
82
|
+
return nil
|
83
|
+
end
|
84
|
+
|
85
|
+
texto_ia = json["candidates"].first.dig("content", "parts", 0, "text")
|
86
|
+
if texto_ia
|
87
|
+
# Sanitiza o texto para garantir formato Gherkin correto
|
88
|
+
texto_limpo = Bddgenx::GherkinCleaner.limpar(texto_ia)
|
89
|
+
Utils::StepCleaner.remover_steps_duplicados(texto_ia, idioma)
|
90
|
+
|
91
|
+
# Insere a diretiva language dinamicamente com base no idioma detectado
|
92
|
+
texto_limpo.sub!(/^# language: .*/, "# language: #{idioma}")
|
93
|
+
texto_limpo.prepend("# language: #{idioma}\n") unless texto_limpo.start_with?("# language:")
|
94
|
+
|
95
|
+
return texto_limpo
|
96
|
+
else
|
97
|
+
warn "❌ Resposta da IA sem conteúdo de texto"
|
98
|
+
warn JSON.pretty_generate(json)
|
99
|
+
return nil
|
100
|
+
end
|
101
|
+
else
|
102
|
+
warn "❌ Erro ao chamar Gemini: #{response.code} - #{response.body}"
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.detecta_idioma_arquivo(caminho_arquivo)
|
108
|
+
return 'pt' unless File.exist?(caminho_arquivo)
|
109
|
+
|
110
|
+
File.foreach(caminho_arquivo) do |linha|
|
111
|
+
if linha =~ /^#\s*language:\s*(\w{2})/i
|
112
|
+
return $1.downcase
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
'pt' # padrão
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# lib/bddgenx/cli.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# Este arquivo define a classe Runner (CLI) da gem bddgenx,
|
5
|
+
# responsável por orquestrar o fluxo de leitura de histórias,
|
6
|
+
# validação, geração de features, steps, backups e exportação de PDFs.
|
7
|
+
require 'dotenv/load'
|
8
|
+
require 'fileutils'
|
9
|
+
require_relative 'utils/parser'
|
10
|
+
require_relative 'generator'
|
11
|
+
require_relative 'utils/pdf_exporter'
|
12
|
+
require_relative 'steps_generator'
|
13
|
+
require_relative 'utils/validator'
|
14
|
+
require_relative 'utils/backup'
|
15
|
+
require_relative 'ia/gemini_cliente'
|
16
|
+
require_relative 'utils/gherkin_cleaner'
|
17
|
+
require_relative 'gemini_generator'
|
18
|
+
|
19
|
+
module Bddgenx
|
20
|
+
# Ponto de entrada da gem: coordena todo o processo de geração BDD.
|
21
|
+
class Runner
|
22
|
+
def self.choose_files(input_dir)
|
23
|
+
ARGV.any? ? selecionar_arquivos_txt(input_dir) : choose_input(input_dir)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.selecionar_arquivos_txt(input_dir)
|
27
|
+
ARGV.map do |arg|
|
28
|
+
nome = arg.end_with?('.txt') ? arg : "#{arg}.txt"
|
29
|
+
path = File.join(input_dir, nome)
|
30
|
+
unless File.exist?(path)
|
31
|
+
warn "⚠️ Arquivo não encontrado: #{path}"
|
32
|
+
next
|
33
|
+
end
|
34
|
+
path
|
35
|
+
end.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.choose_input(input_dir)
|
39
|
+
files = Dir.glob(File.join(input_dir, '*.txt'))
|
40
|
+
if files.empty?
|
41
|
+
warn "❌ Não há arquivos .txt no diretório #{input_dir}"; exit 1
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "Selecione o arquivo de história para processar:"
|
45
|
+
files.each_with_index { |f, i| puts "#{i+1}. #{File.basename(f)}" }
|
46
|
+
print "Digite o número correspondente (ou ENTER para todos): "
|
47
|
+
choice = STDIN.gets.chomp
|
48
|
+
|
49
|
+
return files if choice.empty?
|
50
|
+
idx = choice.to_i - 1
|
51
|
+
unless idx.between?(0, files.size - 1)
|
52
|
+
warn "❌ Escolha inválida."; exit 1
|
53
|
+
end
|
54
|
+
[files[idx]]
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.execute
|
58
|
+
modo = ENV['BDDGENX_MODE'] || 'static'
|
59
|
+
|
60
|
+
input_dir = 'input'
|
61
|
+
Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
|
62
|
+
|
63
|
+
arquivos = choose_files(input_dir)
|
64
|
+
if arquivos.empty?
|
65
|
+
warn "❌ Nenhum arquivo de história para processar."; exit 1
|
66
|
+
end
|
67
|
+
|
68
|
+
total = features = steps = ignored = 0
|
69
|
+
skipped_steps = []
|
70
|
+
generated_pdfs = []
|
71
|
+
skipped_pdfs = []
|
72
|
+
|
73
|
+
arquivos.each do |arquivo|
|
74
|
+
total += 1
|
75
|
+
puts "\n🔍 Processando: #{arquivo}"
|
76
|
+
|
77
|
+
historia =
|
78
|
+
if modo == 'gemini'
|
79
|
+
puts "🤖 Gerando cenários com IA (Gemini)..."
|
80
|
+
begin
|
81
|
+
idioma = GeminiCliente.detecta_idioma_arquivo(arquivo)
|
82
|
+
historia = File.read(arquivo)
|
83
|
+
GeminiCliente.gerar_cenarios(historia, idioma)
|
84
|
+
rescue => e
|
85
|
+
ignored += 1
|
86
|
+
puts "❌ Falha ao gerar com Gemini: #{e.message}"
|
87
|
+
next
|
88
|
+
end
|
89
|
+
else
|
90
|
+
historia = Parser.ler_historia(arquivo)
|
91
|
+
unless Validator.validar(historia)
|
92
|
+
ignored += 1
|
93
|
+
puts "❌ História inválida: #{arquivo}"
|
94
|
+
next
|
95
|
+
end
|
96
|
+
historia
|
97
|
+
end
|
98
|
+
|
99
|
+
historia_limpa = GherkinCleaner.limpar(historia_ia_gerada)
|
100
|
+
feature_path, feature_content = Generator.gerar_feature(historia_limpa)
|
101
|
+
|
102
|
+
Backup.salvar_versao_antiga(feature_path)
|
103
|
+
features += 1 if Generator.salvar_feature(feature_path, feature_content)
|
104
|
+
|
105
|
+
if StepsGenerator.gerar_passos(feature_path)
|
106
|
+
steps += 1
|
107
|
+
else
|
108
|
+
skipped_steps << feature_path
|
109
|
+
end
|
110
|
+
|
111
|
+
FileUtils.mkdir_p('reports')
|
112
|
+
result = PDFExporter.exportar_todos(only_new: true)
|
113
|
+
generated_pdfs.concat(result[:generated])
|
114
|
+
skipped_pdfs.concat(result[:skipped])
|
115
|
+
end
|
116
|
+
|
117
|
+
puts "\n✅ Processamento concluído"
|
118
|
+
puts "- Total de histórias: #{total}"
|
119
|
+
puts "- Features geradas: #{features}"
|
120
|
+
puts "- Steps gerados: #{steps}"
|
121
|
+
puts "- Steps ignorados: #{skipped_steps.size}"
|
122
|
+
puts "- PDFs gerados: #{generated_pdfs.size}"
|
123
|
+
puts "- PDFs já existentes: #{skipped_pdfs.size}"
|
124
|
+
puts "- Histórias ignoradas: #{ignored}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/bddgenx/runner.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# Este arquivo define a classe Runner (CLI) da gem bddgenx,
|
5
5
|
# responsável por orquestrar o fluxo de leitura de histórias,
|
6
6
|
# validação, geração de features, steps, backups e exportação de PDFs.
|
7
|
-
|
7
|
+
require 'dotenv/load'
|
8
8
|
require 'fileutils'
|
9
9
|
require_relative 'utils/parser'
|
10
10
|
require_relative 'generator'
|
@@ -12,6 +12,8 @@ require_relative 'utils/pdf_exporter'
|
|
12
12
|
require_relative 'steps_generator'
|
13
13
|
require_relative 'utils/validator'
|
14
14
|
require_relative 'utils/backup'
|
15
|
+
require_relative 'ia/gemini_cliente'
|
16
|
+
require_relative 'utils/gherkin_cleaner'
|
15
17
|
|
16
18
|
module Bddgenx
|
17
19
|
# Ponto de entrada da gem: coordena todo o processo de geração BDD.
|
@@ -80,6 +82,8 @@ module Bddgenx
|
|
80
82
|
#
|
81
83
|
# @return [void]
|
82
84
|
def self.execute
|
85
|
+
modo = ENV['BDDGENX_MODE'] || 'static'
|
86
|
+
|
83
87
|
input_dir = 'input'
|
84
88
|
Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
|
85
89
|
|
@@ -106,7 +110,38 @@ module Bddgenx
|
|
106
110
|
end
|
107
111
|
|
108
112
|
# Geração de feature
|
109
|
-
|
113
|
+
if modo == 'gemini'
|
114
|
+
puts "🤖 Gerando cenários com IA (Gemini)..."
|
115
|
+
idioma = IA::GeminiCliente.detecta_idioma_arquivo(arquivo) # Seu método que detecta o idioma no .txt (ex: 'pt' ou 'en')
|
116
|
+
spinner = Thread.new do
|
117
|
+
loop do
|
118
|
+
print "\r⏳ Aguardando resposta da IA "
|
119
|
+
3.times do |i|
|
120
|
+
print "." * (i + 1)
|
121
|
+
sleep(0.4)
|
122
|
+
print "\r⏳ Aguardando resposta da IA#{'.' * (i + 1)} "
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
begin
|
127
|
+
feature_text = IA::GeminiCliente.gerar_cenarios(historia, idioma)
|
128
|
+
ensure
|
129
|
+
Thread.kill(spinner)
|
130
|
+
print "\r✅ Resposta da IA recebida! \n"
|
131
|
+
end
|
132
|
+
# feature_text = IA::GeminiCliente.gerar_cenarios(historia, idioma)
|
133
|
+
if feature_text
|
134
|
+
feature_path = Generator.path_para_feature(arquivo)
|
135
|
+
feature_content = Bddgenx::GherkinCleaner.limpar(feature_text)
|
136
|
+
else
|
137
|
+
ignored += 1
|
138
|
+
puts "❌ Falha ao gerar com IA: #{arquivo}"
|
139
|
+
next
|
140
|
+
end
|
141
|
+
else
|
142
|
+
feature_path, feature_content = Generator.gerar_feature(historia)
|
143
|
+
end
|
144
|
+
|
110
145
|
Backup.salvar_versao_antiga(feature_path)
|
111
146
|
features += 1 if Generator.salvar_feature(feature_path, feature_content)
|
112
147
|
|
@@ -8,80 +8,54 @@
|
|
8
8
|
|
9
9
|
require 'fileutils'
|
10
10
|
require 'strscan' # Para uso de StringScanner
|
11
|
+
require 'set'
|
11
12
|
|
12
13
|
module Bddgenx
|
13
|
-
# Gera arquivos de definições de passos Ruby para Cucumber
|
14
|
-
# com base em arquivos .feature.
|
15
14
|
class StepsGenerator
|
16
|
-
# Palavras-chave Gherkin em Português usadas em arquivos de feature
|
17
|
-
# @return [Array<String>]
|
18
15
|
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
19
|
-
|
20
|
-
# Palavras-chave Gherkin em Inglês usadas em arquivos de feature
|
21
|
-
# @return [Array<String>]
|
22
16
|
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
23
|
-
|
24
|
-
# Conjunto de todas as palavras-chave suportadas (PT + EN)
|
25
|
-
# @return [Array<String>]
|
26
17
|
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
27
18
|
|
28
|
-
# Converte uma string para camelCase, útil para nomes de argumentos
|
29
|
-
#
|
30
|
-
# @param [String] str Texto de entrada a ser convertido
|
31
|
-
# @return [String] Versão em camelCase do texto
|
32
19
|
def self.camelize(str)
|
33
20
|
partes = str.strip.split(/[^a-zA-Z0-9]+/)
|
34
21
|
partes.map.with_index { |palavra, i| i.zero? ? palavra.downcase : palavra.capitalize }.join
|
35
22
|
end
|
36
23
|
|
37
|
-
# Gera definições de passos a partir de um arquivo .feature
|
38
|
-
#
|
39
|
-
# @param [String] feature_path Caminho para o arquivo .feature
|
40
|
-
# @raise [ArgumentError] Se feature_path não for String
|
41
|
-
# @return [Boolean] Retorna true se passos foram gerados, false se não houver passos
|
42
24
|
def self.gerar_passos(feature_path)
|
43
|
-
|
44
|
-
unless feature_path.is_a?(String)
|
45
|
-
raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}"
|
46
|
-
end
|
25
|
+
raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
|
47
26
|
|
48
27
|
linhas = File.readlines(feature_path)
|
49
28
|
|
50
|
-
# Detecta idioma no cabeçalho: "# language: pt" ou "# language: en"
|
51
29
|
lang = if (m = linhas.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
|
52
30
|
m[/^#\s*language:\s*(\w+)/i, 1].downcase
|
53
31
|
else
|
54
32
|
'pt'
|
55
33
|
end
|
56
34
|
|
57
|
-
# Mapas de tradução entre PT e EN
|
58
35
|
pt_para_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
59
36
|
en_para_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
60
37
|
|
61
|
-
# Seleciona linhas que começam com palavras-chave Gherkin
|
62
38
|
linhas_passos = linhas.map(&:strip).select do |linha|
|
63
39
|
ALL_KEYS.any? { |chave| linha.start_with?(chave + ' ') }
|
64
40
|
end
|
65
41
|
|
66
|
-
# Se não encontrar passos, retorna false
|
67
42
|
return false if linhas_passos.empty?
|
68
43
|
|
69
|
-
# Cria diretório e arquivo de saída
|
70
44
|
dir_saida = File.join(File.dirname(feature_path), 'steps')
|
71
45
|
FileUtils.mkdir_p(dir_saida)
|
72
46
|
arquivo_saida = File.join(dir_saida, "#{File.basename(feature_path, '.feature')}_steps.rb")
|
73
47
|
|
74
|
-
# Cabeçalho do arquivo gerado
|
75
48
|
conteudo = +"# encoding: utf-8\n"
|
76
49
|
conteudo << "# Definições de passos geradas automaticamente para #{File.basename(feature_path)}\n\n"
|
77
50
|
|
51
|
+
passos_unicos = Set.new
|
52
|
+
|
78
53
|
linhas_passos.each do |linha|
|
79
54
|
palavra_original, restante = linha.split(' ', 2)
|
80
55
|
|
81
|
-
# Define palavra-chave no idioma de saída
|
82
56
|
chave = case lang
|
83
57
|
when 'en' then pt_para_en[palavra_original] || palavra_original
|
84
|
-
else
|
58
|
+
else en_para_pt[palavra_original] || palavra_original
|
85
59
|
end
|
86
60
|
|
87
61
|
texto_bruto = restante.dup
|
@@ -111,11 +85,14 @@ module Bddgenx
|
|
111
85
|
end
|
112
86
|
end
|
113
87
|
|
114
|
-
# Escapa aspas no padrão
|
115
88
|
padrao_seguro = padrao.gsub('"', '\\"')
|
116
|
-
assinatura = "#{chave}(\"#{padrao_seguro}\")"
|
117
89
|
|
118
|
-
#
|
90
|
+
# Impede duplicatas
|
91
|
+
next if passos_unicos.include?(padrao_seguro)
|
92
|
+
|
93
|
+
passos_unicos << padrao_seguro
|
94
|
+
|
95
|
+
assinatura = "#{chave}(\"#{padrao_seguro}\")"
|
119
96
|
if tokens.any?
|
120
97
|
argumentos = tokens.each_index.map { |i| "arg#{i+1}" }.join(', ')
|
121
98
|
assinatura << " do |#{argumentos}|"
|
@@ -128,7 +105,6 @@ module Bddgenx
|
|
128
105
|
conteudo << "end\n\n"
|
129
106
|
end
|
130
107
|
|
131
|
-
# Escreve arquivo de saída
|
132
108
|
File.write(arquivo_saida, conteudo)
|
133
109
|
puts "✅ Steps gerados: #{arquivo_saida}"
|
134
110
|
true
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'unicode'
|
3
|
+
|
4
|
+
module Bddgenx
|
5
|
+
class GherkinCleaner
|
6
|
+
def self.limpar(texto)
|
7
|
+
texto = remover_blocos_markdown(texto)
|
8
|
+
texto = corrigir_language(texto)
|
9
|
+
texto = corrigir_indentacao(texto)
|
10
|
+
texto.strip
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.remover_blocos_markdown(texto)
|
14
|
+
texto.gsub(/```[a-z]*\n?/i, '').gsub(/```/, '')
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.corrigir_language(texto)
|
18
|
+
linhas = texto.lines
|
19
|
+
primeira_language = linhas.find { |linha| linha.strip.start_with?('# language:') }
|
20
|
+
|
21
|
+
# Remove duplicações de # language
|
22
|
+
linhas.reject! { |linha| linha.strip.start_with?('# language:') }
|
23
|
+
|
24
|
+
if primeira_language
|
25
|
+
linhas.unshift(primeira_language.strip + "\n")
|
26
|
+
else
|
27
|
+
idioma = detectar_idioma(linhas.join)
|
28
|
+
linhas.unshift("# language: #{idioma}\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
linhas.join
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.detectar_idioma(texto)
|
35
|
+
return 'pt' if texto =~ /Dado|Quando|Então|E /i
|
36
|
+
return 'en' if texto =~ /Given|When|Then|And /i
|
37
|
+
'pt' # padrão
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.corrigir_indentacao(texto)
|
41
|
+
linhas = texto.lines.map do |linha|
|
42
|
+
if linha.strip.start_with?('Feature', 'Funcionalidade')
|
43
|
+
linha.strip + "\n"
|
44
|
+
elsif linha.strip.start_with?('Scenario', 'Cenário', 'Scenario Outline', 'Esquema do Cenário')
|
45
|
+
" #{linha.strip}\n"
|
46
|
+
elsif linha.strip.start_with?('Given', 'When', 'Then', 'And', 'Dado', 'Quando', 'Então', 'E')
|
47
|
+
" #{linha.strip}\n"
|
48
|
+
elsif linha.strip.start_with?('|')
|
49
|
+
" #{linha.strip}\n"
|
50
|
+
else
|
51
|
+
" #{linha.strip}\n"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
linhas.join
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/bddgenx/utils/parser.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
# arquivos de história (.txt), extraindo cabeçalho e blocos de passos e exemplos.
|
6
6
|
# Utiliza constantes para identificação de tipos de blocos e suporta idiomas
|
7
7
|
# Português e Inglês na marcação de idioma e blocos de exemplos.
|
8
|
+
require_relative '../ia/gemini_cliente'
|
8
9
|
|
9
10
|
module Bddgenx
|
10
11
|
# Tipos de blocos reconhecidos na história (.txt), incluindo variações em Português
|
@@ -42,10 +43,13 @@ module Bddgenx
|
|
42
43
|
def self.ler_historia(caminho_arquivo)
|
43
44
|
# Carrega linhas do arquivo, preservando linhas vazias e encoding UTF-8
|
44
45
|
linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:rstrip)
|
46
|
+
# parser.rb (durante leitura do .txt)
|
47
|
+
primeira_linha = File.readlines(caminho_arquivo).first
|
48
|
+
idioma_forcado = primeira_linha&.match(/^#\s*lang(?:uage)?\s*:\s*(\w+)/i)&.captures&.first
|
45
49
|
|
46
50
|
# Detecta idioma no topo do arquivo: suporta '# lang: <codigo>' ou '# language: <codigo>'
|
47
51
|
idioma = 'pt'
|
48
|
-
if linhas.first
|
52
|
+
if linhas.first == idioma_forcado
|
49
53
|
idioma = Regexp.last_match(1).downcase
|
50
54
|
linhas.shift
|
51
55
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'unicode'
|
3
|
+
|
4
|
+
module Bddgenx
|
5
|
+
module Utils
|
6
|
+
class StepCleaner
|
7
|
+
def self.remover_steps_duplicados(texto, idioma)
|
8
|
+
keywords = idioma == 'en' ? %w[Given When Then And] : %w[Dado Quando Então E]
|
9
|
+
seen = Set.new
|
10
|
+
resultado = []
|
11
|
+
|
12
|
+
texto.each_line do |linha|
|
13
|
+
if keywords.any? { |kw| linha.strip.start_with?(kw) }
|
14
|
+
canonical = canonicalize_step(linha, keywords)
|
15
|
+
unless seen.include?(canonical)
|
16
|
+
seen.add(canonical)
|
17
|
+
resultado << linha
|
18
|
+
end
|
19
|
+
else
|
20
|
+
resultado << linha
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
resultado.join
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.canonicalize_step(linha, keywords)
|
28
|
+
# Remove keyword
|
29
|
+
texto = linha.dup.strip
|
30
|
+
keywords.each do |kw|
|
31
|
+
texto.sub!(/^#{kw}\s+/i, '')
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generaliza: substitui textos entre aspas, colchetes e números por <param>
|
35
|
+
texto.gsub!(/"[^"]*"|<[^>]*>|\b\d+\b/, '<param>')
|
36
|
+
|
37
|
+
# Remove acentos e pontuação, normaliza espaços
|
38
|
+
texto = Unicode.normalize_KD(texto).gsub(/\p{Mn}/, '')
|
39
|
+
texto.gsub!(/[^a-zA-Z0-9\s<>]/, '')
|
40
|
+
texto.downcase.strip.squeeze(" ")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
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.
|
4
|
+
version: 1.0.0
|
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-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: prawn
|
@@ -28,28 +28,28 @@ dependencies:
|
|
28
28
|
name: prawn-table
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.2.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.2.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: prawn-svg
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: 0.2.2
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.2.2
|
55
55
|
description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
|
@@ -71,12 +71,16 @@ files:
|
|
71
71
|
- lib/bddgenx/assets/fonts/DejaVuSansMono-Oblique.ttf
|
72
72
|
- lib/bddgenx/assets/fonts/DejaVuSansMono.ttf
|
73
73
|
- lib/bddgenx/generator.rb
|
74
|
+
- lib/bddgenx/ia/gemini_cliente.rb
|
75
|
+
- lib/bddgenx/ia/gemini_generator.rb
|
74
76
|
- lib/bddgenx/runner.rb
|
75
77
|
- lib/bddgenx/steps_generator.rb
|
76
78
|
- lib/bddgenx/utils/backup.rb
|
77
|
-
- lib/bddgenx/utils/
|
79
|
+
- lib/bddgenx/utils/font_loader.rb
|
80
|
+
- lib/bddgenx/utils/gherkin_cleaner.rb
|
78
81
|
- lib/bddgenx/utils/parser.rb
|
79
82
|
- lib/bddgenx/utils/pdf_exporter.rb
|
83
|
+
- lib/bddgenx/utils/remover_steps_duplicados.rb
|
80
84
|
- lib/bddgenx/utils/tracer.rb
|
81
85
|
- lib/bddgenx/utils/validator.rb
|
82
86
|
- lib/bddgenx/version.rb
|
File without changes
|