bddgenx 0.1.51 → 2.0.5
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 +50 -0
- data/VERSION +1 -1
- data/bin/bddgenx +1 -1
- data/lib/bddgenx/generators/generator.rb +175 -0
- data/lib/bddgenx/{runner.rb → generators/runner.rb} +24 -9
- data/lib/bddgenx/{steps_generator.rb → generators/steps_generator.rb} +32 -34
- data/lib/bddgenx/ia/chatgtp_cliente.rb +147 -0
- data/lib/bddgenx/ia/gemini_cliente.rb +135 -0
- data/lib/bddgenx/{utils → reports}/backup.rb +0 -4
- data/lib/bddgenx/{utils → reports}/pdf_exporter.rb +0 -5
- data/lib/bddgenx/{utils → reports}/tracer.rb +0 -4
- data/lib/bddgenx/{utils/fontLoader.rb → support/font_loader.rb} +0 -4
- data/lib/bddgenx/support/gherkin_cleaner.rb +102 -0
- data/lib/bddgenx/support/remover_steps_duplicados.rb +81 -0
- data/lib/bddgenx/{utils → support}/validator.rb +0 -1
- data/lib/bddgenx.rb +11 -2
- data/lib/env.rb +46 -0
- data/lib/{bddgenx/utils/parser.rb → parser.rb} +5 -3
- data/lib/version.rb +14 -0
- metadata +73 -12
- data/lib/bddgenx/generator.rb +0 -155
- data/lib/bddgenx/version.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ee7808e2b0f0cf6e65b1dbc5ea11689a1522dbc06ec2db6ed35494329920913
|
4
|
+
data.tar.gz: f88f37264c7e26caa90f4b25aef60e194cd5b08d7a97819b4aa623a7e99d6b04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bdc7124701b3fd7eba6e60993e75e31fb19613a1ae53cb7549afeae1c0d4a7f167a3e5c19bf6805ee118ffd6a6c7a6b5d29b0d25e34e894dbfb86c0e45f2879
|
7
|
+
data.tar.gz: eec2871ad622cb5c638163854dbb7dabcb5da57e6bb784732c561c50cc1a4fabc81c3647b82c63509a6bd125e72801db2a69a88415d550af44ccf9f944ed7ac7
|
data/README.md
CHANGED
@@ -5,6 +5,56 @@
|
|
5
5
|
|
6
6
|
Ferramenta Ruby para gerar automaticamente arquivos Gherkin (`.feature`) e definições de passos (`steps.rb`) a partir de histórias em texto. Atende aos padrões ISTQB, suporta parametrização com blocos de exemplos e fornece relatórios de QA (rastreabilidade, backups e PDF).
|
7
7
|
|
8
|
+
## Estrutura do projeto
|
9
|
+
```
|
10
|
+
bdd-generation/
|
11
|
+
├── .github/ # Workflows do GitHub Actions
|
12
|
+
│ └── workflows/
|
13
|
+
│ └── main.yml
|
14
|
+
├── bin/
|
15
|
+
│ └── console # Execução local (se necessário)
|
16
|
+
├── features/ # Gherkin gerados automaticamente
|
17
|
+
│ └── steps/ # Steps correspondentes
|
18
|
+
├── input/ # Histórias de entrada
|
19
|
+
│ ├── historia.txt
|
20
|
+
│ ├── historia_en.txt
|
21
|
+
│ └── ...
|
22
|
+
├── lib/
|
23
|
+
│ └── bddgenx/
|
24
|
+
│ ├── ia/ # Módulo de IA
|
25
|
+
│ │ ├── chatgpt_cliente.rb
|
26
|
+
│ │ └── gemini_cliente.rb
|
27
|
+
│ ├── generators/ # Responsável por geração
|
28
|
+
│ │ ├── generator.rb
|
29
|
+
│ │ ├── steps_generator.rb
|
30
|
+
│ │ └── runner.rb
|
31
|
+
│ ├── reports/ # Exportadores, backups e rastreabilidade
|
32
|
+
│ │ ├── pdf_exporter.rb
|
33
|
+
│ │ ├── backup.rb
|
34
|
+
│ │ └── tracer.rb
|
35
|
+
│ ├── support/ # Utilitários auxiliares
|
36
|
+
│ │ ├── gherkin_cleaner.rb
|
37
|
+
│ │ ├── remover_steps_duplicados.rb
|
38
|
+
│ │ ├── validator.rb
|
39
|
+
│ │ └── font_loader.rb
|
40
|
+
│ ├── parser.rb # Parse do .txt
|
41
|
+
│ ├── version.rb # Versão da gem
|
42
|
+
│ └── bddgenx.rb # Arquivo principal (require central)
|
43
|
+
├── reports/ # Saídas: PDF, backup, rastreabilidade
|
44
|
+
│ ├── pdf/
|
45
|
+
│ ├── backup/
|
46
|
+
│ └── rastreabilidade/
|
47
|
+
├── .env # Variáveis de ambiente
|
48
|
+
├── .gitignore
|
49
|
+
├── bddgenx.gemspec
|
50
|
+
├── bump_version.sh
|
51
|
+
├── Gemfile
|
52
|
+
├── Gemfile.lock
|
53
|
+
├── LICENSE
|
54
|
+
├── Rakefile
|
55
|
+
├── README.md
|
56
|
+
└── VERSION
|
57
|
+
```
|
8
58
|
## Instalação
|
9
59
|
|
10
60
|
Adicione ao seu `Gemfile`:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
2.0.5
|
data/bin/bddgenx
CHANGED
@@ -0,0 +1,175 @@
|
|
1
|
+
# lib/bddgenx/generator.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# Este arquivo define a classe Generator, responsável por gerar arquivos
|
5
|
+
# .feature a partir de um hash de história ou de um arquivo de história em texto.
|
6
|
+
# Suporta Gherkin em Português e Inglês, inclusão de tags, cenários simples
|
7
|
+
# e esquemas de cenário com exemplos.
|
8
|
+
|
9
|
+
module Bddgenx
|
10
|
+
class Generator
|
11
|
+
# Palavras-chave do Gherkin em Português
|
12
|
+
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
13
|
+
|
14
|
+
# Palavras-chave do Gherkin em Inglês
|
15
|
+
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
16
|
+
|
17
|
+
# Mapeamento de PT → EN
|
18
|
+
GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
19
|
+
|
20
|
+
# Mapeamento de EN → PT
|
21
|
+
GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
22
|
+
|
23
|
+
# Todas as palavras-chave reconhecidas
|
24
|
+
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
25
|
+
|
26
|
+
##
|
27
|
+
# Extrai todas as linhas de exemplo de um array de strings.
|
28
|
+
#
|
29
|
+
# @param raw [Array<String>] um array contendo linhas de texto
|
30
|
+
# @return [Array<String>] apenas as linhas que começam com '|', ou seja, exemplos
|
31
|
+
def self.dividir_examples(raw)
|
32
|
+
raw.select { |l| l.strip.start_with?('|') }
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Gera o conteúdo de um arquivo `.feature` a partir de uma história.
|
37
|
+
# Pode operar em três modos: estático (hash ou arquivo estruturado),
|
38
|
+
# IA com Gemini, ou IA com ChatGPT.
|
39
|
+
#
|
40
|
+
# @param input [String, Hash] caminho para um arquivo .txt ou um hash estruturado
|
41
|
+
# @param override_path [String, nil] caminho alternativo para salvar o arquivo gerado
|
42
|
+
# @return [Array(String, String)] caminho do arquivo gerado e conteúdo do .feature
|
43
|
+
def self.gerar_feature(input, override_path = nil)
|
44
|
+
modo = ENV['BDD_MODE']&.to_sym || :static
|
45
|
+
|
46
|
+
if input.is_a?(String) && input.end_with?('.txt') && [:gemini, :chatgpt].include?(modo)
|
47
|
+
# Modo com IA: gera cenários automaticamente com base no texto da história
|
48
|
+
raw_txt = File.read(input)
|
49
|
+
historia = {
|
50
|
+
idioma: 'pt',
|
51
|
+
quero: File.basename(input, '.txt').tr('_', ' ').capitalize,
|
52
|
+
como: '',
|
53
|
+
para: '',
|
54
|
+
grupos: []
|
55
|
+
}
|
56
|
+
|
57
|
+
texto_gerado = if modo == :gemini
|
58
|
+
GeminiCliente.gerar_cenarios(raw_txt)
|
59
|
+
else
|
60
|
+
ChatGPTCliente.gerar_cenarios(raw_txt)
|
61
|
+
end
|
62
|
+
|
63
|
+
historia[:grupos] << {
|
64
|
+
tipo: 'gerado',
|
65
|
+
tag: 'ia',
|
66
|
+
passos: GherkinCleaner.limpar(texto_gerado).lines.map(&:strip).reject(&:empty?)
|
67
|
+
}
|
68
|
+
else
|
69
|
+
# Modo estático: utiliza estrutura vinda do Parser ou de um hash diretamente
|
70
|
+
historia = input.is_a?(String) ? Parser.ler_historia(input) : input
|
71
|
+
end
|
72
|
+
|
73
|
+
idioma = historia[:idioma] || 'pt'
|
74
|
+
cont = 1
|
75
|
+
|
76
|
+
# Normaliza o nome base do arquivo
|
77
|
+
nome_base = historia[:quero]
|
78
|
+
.gsub(/[^a-z0-9]/i, '_')
|
79
|
+
.downcase
|
80
|
+
.split('_')
|
81
|
+
.reject(&:empty?)
|
82
|
+
.first(5)
|
83
|
+
.join('_')
|
84
|
+
|
85
|
+
caminho = override_path || "features/#{nome_base}.feature"
|
86
|
+
|
87
|
+
# Define palavras-chave com base no idioma
|
88
|
+
palavras = {
|
89
|
+
feature: idioma == 'en' ? 'Feature' : 'Funcionalidade',
|
90
|
+
contexto: idioma == 'en' ? 'Background' : 'Contexto',
|
91
|
+
cenario: idioma == 'en' ? 'Scenario' : 'Cenário',
|
92
|
+
esquema: idioma == 'en' ? 'Scenario Outline' : 'Esquema do Cenário',
|
93
|
+
exemplos: idioma == 'en' ? 'Examples' : 'Exemplos',
|
94
|
+
regra: idioma == 'en' ? 'Rule' : 'Regra'
|
95
|
+
}
|
96
|
+
|
97
|
+
conteudo = <<~GHK
|
98
|
+
# language: #{idioma}
|
99
|
+
#{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
|
100
|
+
# #{historia[:como]}
|
101
|
+
# #{historia[:quero]}
|
102
|
+
# #{historia[:para]}
|
103
|
+
GHK
|
104
|
+
|
105
|
+
passos_unicos = Set.new
|
106
|
+
pt_map = GHERKIN_MAP_PT_EN
|
107
|
+
en_map = GHERKIN_MAP_EN_PT
|
108
|
+
detect = ALL_KEYS
|
109
|
+
cont = 1
|
110
|
+
|
111
|
+
historia[:grupos].each do |grupo|
|
112
|
+
passos = grupo[:passos] || []
|
113
|
+
exemplos = grupo[:exemplos] || []
|
114
|
+
next if passos.empty?
|
115
|
+
|
116
|
+
tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
|
117
|
+
conteudo << " #{tag_line}\n"
|
118
|
+
|
119
|
+
if exemplos.any?
|
120
|
+
conteudo << " #{palavras[:esquema]}: Exemplo #{cont}\n"
|
121
|
+
cont += 1
|
122
|
+
else
|
123
|
+
conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
|
124
|
+
end
|
125
|
+
|
126
|
+
passos.each do |p|
|
127
|
+
parts = p.strip.split(' ', 2)
|
128
|
+
con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
|
129
|
+
text = parts[1] || ''
|
130
|
+
out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
|
131
|
+
linha_step = " #{out_conn} #{text}"
|
132
|
+
|
133
|
+
next if passos_unicos.include?(linha_step)
|
134
|
+
|
135
|
+
passos_unicos << linha_step
|
136
|
+
conteudo << "#{linha_step}\n"
|
137
|
+
end
|
138
|
+
|
139
|
+
if exemplos.any?
|
140
|
+
conteudo << "\n #{palavras[:exemplos]}:\n"
|
141
|
+
exemplos.select { |l| l.strip.start_with?('|') }.each do |line|
|
142
|
+
cleaned = line.strip.gsub(/^"|"$/, '')
|
143
|
+
conteudo << " #{cleaned}\n"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
conteudo << "\n"
|
148
|
+
end
|
149
|
+
|
150
|
+
[caminho, conteudo]
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Gera o caminho padrão de saída para um arquivo `.feature` com base no nome do `.txt`.
|
155
|
+
#
|
156
|
+
# @param arquivo_txt [String] caminho do arquivo .txt de entrada
|
157
|
+
# @return [String] caminho completo do arquivo .feature correspondente
|
158
|
+
def self.path_para_feature(arquivo_txt)
|
159
|
+
nome = File.basename(arquivo_txt, '.txt')
|
160
|
+
File.join('features', "#{nome}.feature")
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Salva o conteúdo gerado no disco, criando diretórios se necessário.
|
165
|
+
#
|
166
|
+
# @param caminho [String] caminho completo para salvar o arquivo
|
167
|
+
# @param conteudo [String] conteúdo do arquivo .feature
|
168
|
+
# @return [void]
|
169
|
+
def self.salvar_feature(caminho, conteudo)
|
170
|
+
FileUtils.mkdir_p(File.dirname(caminho))
|
171
|
+
File.write(caminho, conteudo)
|
172
|
+
puts "✅ Arquivo .feature gerado: #{caminho}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -4,14 +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
|
-
|
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'
|
7
|
+
require_relative '../../bddgenx'
|
15
8
|
|
16
9
|
module Bddgenx
|
17
10
|
# Ponto de entrada da gem: coordena todo o processo de geração BDD.
|
@@ -80,6 +73,8 @@ module Bddgenx
|
|
80
73
|
#
|
81
74
|
# @return [void]
|
82
75
|
def self.execute
|
76
|
+
modo = ENV['BDDGENX_MODE'] || 'static'
|
77
|
+
|
83
78
|
input_dir = 'input'
|
84
79
|
Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
|
85
80
|
|
@@ -106,7 +101,27 @@ module Bddgenx
|
|
106
101
|
end
|
107
102
|
|
108
103
|
# Geração de feature
|
109
|
-
|
104
|
+
if modo == 'gemini' || modo == 'chatgpt'
|
105
|
+
puts "🤖 Gerando cenários com IA (#{modo.capitalize})..."
|
106
|
+
idioma = IA::GeminiCliente.detecta_idioma_arquivo(arquivo)
|
107
|
+
feature_text =
|
108
|
+
if modo == 'gemini'
|
109
|
+
IA::GeminiCliente.gerar_cenarios(historia, idioma)
|
110
|
+
else
|
111
|
+
IA::ChatGptCliente.gerar_cenarios(historia, idioma)
|
112
|
+
end
|
113
|
+
if feature_text
|
114
|
+
feature_path = Generator.path_para_feature(arquivo)
|
115
|
+
feature_content = Bddgenx::GherkinCleaner.limpar(feature_text)
|
116
|
+
else
|
117
|
+
ignored += 1
|
118
|
+
puts "❌ Falha ao gerar com IA: #{arquivo}"
|
119
|
+
next
|
120
|
+
end
|
121
|
+
else
|
122
|
+
feature_path, feature_content = Generator.gerar_feature(historia)
|
123
|
+
end
|
124
|
+
|
110
125
|
Backup.salvar_versao_antiga(feature_path)
|
111
126
|
features += 1 if Generator.salvar_feature(feature_path, feature_content)
|
112
127
|
|
@@ -6,82 +6,77 @@
|
|
6
6
|
# Suporta palavras-chave Gherkin em Português e Inglês e parametriza
|
7
7
|
# strings e números conforme necessário.
|
8
8
|
|
9
|
-
require 'fileutils'
|
10
|
-
require 'strscan' # Para uso de StringScanner
|
11
|
-
|
12
9
|
module Bddgenx
|
13
|
-
# Gera arquivos de definições de passos Ruby para Cucumber
|
14
|
-
# com base em arquivos .feature.
|
15
10
|
class StepsGenerator
|
16
|
-
# Palavras-chave Gherkin em Português
|
17
|
-
# @return [Array<String>]
|
11
|
+
# Palavras-chave Gherkin em Português
|
18
12
|
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
19
13
|
|
20
|
-
# Palavras-chave Gherkin em Inglês
|
21
|
-
# @return [Array<String>]
|
14
|
+
# Palavras-chave Gherkin em Inglês
|
22
15
|
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
23
16
|
|
24
|
-
# Conjunto de todas as palavras-chave suportadas
|
25
|
-
# @return [Array<String>]
|
17
|
+
# Conjunto de todas as palavras-chave suportadas
|
26
18
|
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
27
19
|
|
28
|
-
|
20
|
+
##
|
21
|
+
# Transforma uma string em estilo camelCase.
|
22
|
+
#
|
23
|
+
# @param str [String] A string a ser transformada.
|
24
|
+
# @return [String] A string convertida para camelCase.
|
29
25
|
#
|
30
|
-
# @param [String] str Texto de entrada a ser convertido
|
31
|
-
# @return [String] Versão em camelCase do texto
|
32
26
|
def self.camelize(str)
|
33
27
|
partes = str.strip.split(/[^a-zA-Z0-9]+/)
|
34
28
|
partes.map.with_index { |palavra, i| i.zero? ? palavra.downcase : palavra.capitalize }.join
|
35
29
|
end
|
36
30
|
|
37
|
-
|
31
|
+
##
|
32
|
+
# Gera arquivos de passos do Cucumber a partir de um arquivo .feature.
|
33
|
+
#
|
34
|
+
# O método lê o arquivo, detecta o idioma, extrai os passos,
|
35
|
+
# parametriza as variáveis (números e strings), e escreve os métodos
|
36
|
+
# em um novo arquivo no diretório `steps/`.
|
37
|
+
#
|
38
|
+
# @param feature_path [String] Caminho para o arquivo .feature.
|
39
|
+
# @return [Boolean] Retorna true se os passos forem gerados com sucesso, false se não houver passos.
|
40
|
+
# @raise [ArgumentError] Se o caminho fornecido não for uma String.
|
38
41
|
#
|
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
42
|
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
|
43
|
+
raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
|
47
44
|
|
48
45
|
linhas = File.readlines(feature_path)
|
49
46
|
|
50
|
-
# Detecta idioma
|
47
|
+
# Detecta o idioma com base na diretiva "# language:"
|
51
48
|
lang = if (m = linhas.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
|
52
49
|
m[/^#\s*language:\s*(\w+)/i, 1].downcase
|
53
50
|
else
|
54
51
|
'pt'
|
55
52
|
end
|
56
53
|
|
57
|
-
# Mapas de tradução entre PT e EN
|
58
54
|
pt_para_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
59
55
|
en_para_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
60
56
|
|
61
|
-
# Seleciona linhas que começam com palavras-chave Gherkin
|
57
|
+
# Seleciona apenas linhas que começam com palavras-chave Gherkin
|
62
58
|
linhas_passos = linhas.map(&:strip).select do |linha|
|
63
59
|
ALL_KEYS.any? { |chave| linha.start_with?(chave + ' ') }
|
64
60
|
end
|
65
61
|
|
66
|
-
# Se não encontrar passos, retorna false
|
67
62
|
return false if linhas_passos.empty?
|
68
63
|
|
69
|
-
# Cria diretório e arquivo de saída
|
70
64
|
dir_saida = File.join(File.dirname(feature_path), 'steps')
|
71
65
|
FileUtils.mkdir_p(dir_saida)
|
72
66
|
arquivo_saida = File.join(dir_saida, "#{File.basename(feature_path, '.feature')}_steps.rb")
|
73
67
|
|
74
|
-
# Cabeçalho do arquivo gerado
|
75
68
|
conteudo = +"# encoding: utf-8\n"
|
76
69
|
conteudo << "# Definições de passos geradas automaticamente para #{File.basename(feature_path)}\n\n"
|
77
70
|
|
71
|
+
passos_unicos = Set.new
|
72
|
+
|
78
73
|
linhas_passos.each do |linha|
|
79
74
|
palavra_original, restante = linha.split(' ', 2)
|
80
75
|
|
81
|
-
#
|
76
|
+
# Tradução de palavras-chave se necessário
|
82
77
|
chave = case lang
|
83
78
|
when 'en' then pt_para_en[palavra_original] || palavra_original
|
84
|
-
else
|
79
|
+
else en_para_pt[palavra_original] || palavra_original
|
85
80
|
end
|
86
81
|
|
87
82
|
texto_bruto = restante.dup
|
@@ -89,6 +84,7 @@ module Bddgenx
|
|
89
84
|
padrao = ''
|
90
85
|
tokens = []
|
91
86
|
|
87
|
+
# Analisa e parametriza o conteúdo dos passos
|
92
88
|
until scanner.eos?
|
93
89
|
if scanner.check(/"<([^>]+)>"/)
|
94
90
|
scanner.scan(/"<([^>]+)>"/)
|
@@ -111,11 +107,14 @@ module Bddgenx
|
|
111
107
|
end
|
112
108
|
end
|
113
109
|
|
114
|
-
# Escapa aspas no padrão
|
115
110
|
padrao_seguro = padrao.gsub('"', '\\"')
|
116
|
-
assinatura = "#{chave}(\"#{padrao_seguro}\")"
|
117
111
|
|
118
|
-
#
|
112
|
+
# Impede criação de métodos duplicados
|
113
|
+
next if passos_unicos.include?(padrao_seguro)
|
114
|
+
|
115
|
+
passos_unicos << padrao_seguro
|
116
|
+
|
117
|
+
assinatura = "#{chave}(\"#{padrao_seguro}\")"
|
119
118
|
if tokens.any?
|
120
119
|
argumentos = tokens.each_index.map { |i| "arg#{i+1}" }.join(', ')
|
121
120
|
assinatura << " do |#{argumentos}|"
|
@@ -128,7 +127,6 @@ module Bddgenx
|
|
128
127
|
conteudo << "end\n\n"
|
129
128
|
end
|
130
129
|
|
131
|
-
# Escreve arquivo de saída
|
132
130
|
File.write(arquivo_saida, conteudo)
|
133
131
|
puts "✅ Steps gerados: #{arquivo_saida}"
|
134
132
|
true
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# lib/bddgenx/ia/chatgpt_cliente.rb
|
2
|
+
|
3
|
+
module Bddgenx
|
4
|
+
module IA
|
5
|
+
##
|
6
|
+
# Cliente para interação com a API do ChatGPT da OpenAI para gerar
|
7
|
+
# cenários BDD no formato Gherkin, com suporte a fallback para Gemini.
|
8
|
+
#
|
9
|
+
class ChatGptCliente
|
10
|
+
CHATGPT_API_URL = 'https://api.openai.com/v1/chat/completions'.freeze
|
11
|
+
MODEL = 'gpt-4o'
|
12
|
+
|
13
|
+
##
|
14
|
+
# Gera cenários BDD a partir de uma história fornecida,
|
15
|
+
# solicitando à API do ChatGPT a criação dos cenários em formato Gherkin.
|
16
|
+
# Se a API key não estiver configurada ou houver erro na requisição,
|
17
|
+
# utiliza fallback com o GeminiCliente.
|
18
|
+
#
|
19
|
+
# @param historia [String] Texto com a história para basear os cenários.
|
20
|
+
# @param idioma [String] Código do idioma ('pt' ou 'en'), padrão 'pt'.
|
21
|
+
# @return [String] Cenários gerados em formato Gherkin com palavras-chave no idioma indicado.
|
22
|
+
#
|
23
|
+
def self.gerar_cenarios(historia, idioma = 'pt')
|
24
|
+
api_key = ENV['OPENAI_API_KEY']
|
25
|
+
|
26
|
+
unless api_key
|
27
|
+
warn "❌ API Key do ChatGPT não encontrada no .env (OPENAI_API_KEY)"
|
28
|
+
return fallback_com_gemini(historia, idioma)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Palavras-chave Gherkin para português e inglês
|
32
|
+
keywords_pt = {
|
33
|
+
feature: "Funcionalidade",
|
34
|
+
scenario: "Cenário",
|
35
|
+
scenario_outline: "Esquema do Cenário",
|
36
|
+
examples: "Exemplos",
|
37
|
+
given: "Dado",
|
38
|
+
when: "Quando",
|
39
|
+
then: "Então",
|
40
|
+
and: "E"
|
41
|
+
}
|
42
|
+
|
43
|
+
keywords_en = {
|
44
|
+
feature: "Feature",
|
45
|
+
scenario: "Scenario",
|
46
|
+
scenario_outline: "Scenario Outline",
|
47
|
+
examples: "Examples",
|
48
|
+
given: "Given",
|
49
|
+
when: "When",
|
50
|
+
then: "Then",
|
51
|
+
and: "And"
|
52
|
+
}
|
53
|
+
|
54
|
+
keywords = idioma == 'en' ? keywords_en : keywords_pt
|
55
|
+
|
56
|
+
# Prompt base enviado ao ChatGPT, instruindo a saída no formato correto
|
57
|
+
prompt_base = <<~PROMPT
|
58
|
+
Gere cenários BDD no formato Gherkin, usando as palavras-chave de estrutura no idioma \"#{idioma}\":
|
59
|
+
Feature: #{keywords[:feature]}
|
60
|
+
Scenario: #{keywords[:scenario]}
|
61
|
+
Scenario Outline: #{keywords[:scenario_outline]}
|
62
|
+
Examples: #{keywords[:examples]}
|
63
|
+
Given: #{keywords[:given]}
|
64
|
+
When: #{keywords[:when]}
|
65
|
+
Then: #{keywords[:then]}
|
66
|
+
And: #{keywords[:and]}
|
67
|
+
|
68
|
+
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.
|
69
|
+
|
70
|
+
História:
|
71
|
+
#{historia}
|
72
|
+
PROMPT
|
73
|
+
|
74
|
+
uri = URI(CHATGPT_API_URL)
|
75
|
+
request_body = {
|
76
|
+
model: MODEL,
|
77
|
+
messages: [
|
78
|
+
{
|
79
|
+
role: "user",
|
80
|
+
content: prompt_base
|
81
|
+
}
|
82
|
+
]
|
83
|
+
}
|
84
|
+
|
85
|
+
headers = {
|
86
|
+
"Content-Type" => "application/json",
|
87
|
+
"Authorization" => "Bearer #{api_key}"
|
88
|
+
}
|
89
|
+
|
90
|
+
response = Net::HTTP.post(uri, request_body.to_json, headers)
|
91
|
+
|
92
|
+
if response.is_a?(Net::HTTPSuccess)
|
93
|
+
json = JSON.parse(response.body)
|
94
|
+
texto_ia = json.dig("choices", 0, "message", "content")
|
95
|
+
|
96
|
+
if texto_ia
|
97
|
+
texto_limpo = Bddgenx::GherkinCleaner.limpar(texto_ia)
|
98
|
+
Utils::StepCleaner.remover_steps_duplicados(texto_ia, idioma)
|
99
|
+
|
100
|
+
# Ajusta a linha de idioma no arquivo gerado
|
101
|
+
texto_limpo.sub!(/^# language: .*/, "# language: #{idioma}")
|
102
|
+
texto_limpo.prepend("# language: #{idioma}\n") unless texto_limpo.start_with?("# language:")
|
103
|
+
return texto_limpo
|
104
|
+
else
|
105
|
+
warn "❌ Resposta da IA sem conteúdo de texto"
|
106
|
+
warn JSON.pretty_generate(json)
|
107
|
+
return fallback_com_gemini(historia, idioma)
|
108
|
+
end
|
109
|
+
else
|
110
|
+
warn "❌ Erro ao chamar ChatGPT: #{response.code} - #{response.body}"
|
111
|
+
return fallback_com_gemini(historia, idioma)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Método de fallback que chama o GeminiCliente para gerar cenários,
|
117
|
+
# usado quando a API do ChatGPT não está disponível ou ocorre erro.
|
118
|
+
#
|
119
|
+
# @param historia [String] Texto da história para basear os cenários.
|
120
|
+
# @param idioma [String] Código do idioma ('pt' ou 'en').
|
121
|
+
# @return [String] Cenários gerados pelo GeminiCliente.
|
122
|
+
#
|
123
|
+
def self.fallback_com_gemini(historia, idioma)
|
124
|
+
warn "🔁 Tentando gerar com Gemini como fallback..."
|
125
|
+
GeminiCliente.gerar_cenarios(historia, idioma)
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Detecta o idioma de um arquivo de feature pela linha "# language:".
|
130
|
+
#
|
131
|
+
# @param caminho_arquivo [String] Caminho para o arquivo de feature.
|
132
|
+
# @return [String] Código do idioma detectado ('pt' por padrão).
|
133
|
+
#
|
134
|
+
def self.detecta_idioma_arquivo(caminho_arquivo)
|
135
|
+
return 'pt' unless File.exist?(caminho_arquivo)
|
136
|
+
|
137
|
+
File.foreach(caminho_arquivo) do |linha|
|
138
|
+
if linha =~ /^#\s*language:\s*(\w{2})/i
|
139
|
+
return $1.downcase
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
'pt'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|