bddgenx 1.0.0 → 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/{generator.rb → generators/generator.rb} +42 -18
- data/lib/bddgenx/{runner.rb → generators/runner.rb} +9 -29
- data/lib/bddgenx/{steps_generator.rb → generators/steps_generator.rb} +27 -5
- data/lib/bddgenx/ia/chatgtp_cliente.rb +147 -0
- data/lib/bddgenx/ia/gemini_cliente.rb +26 -11
- 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 → 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} +1 -3
- data/lib/version.rb +14 -0
- metadata +71 -14
- data/lib/bddgenx/ia/gemini_generator.rb +0 -127
- data/lib/bddgenx/utils/gherkin_cleaner.rb +0 -57
- data/lib/bddgenx/utils/remover_steps_duplicados.rb +0 -44
- 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
|
-
|
1
|
+
2.0.5
|
data/bin/bddgenx
CHANGED
@@ -6,30 +6,45 @@
|
|
6
6
|
# Suporta Gherkin em Português e Inglês, inclusão de tags, cenários simples
|
7
7
|
# e esquemas de cenário com exemplos.
|
8
8
|
|
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'
|
15
|
-
|
16
9
|
module Bddgenx
|
17
10
|
class Generator
|
11
|
+
# Palavras-chave do Gherkin em Português
|
18
12
|
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
13
|
+
|
14
|
+
# Palavras-chave do Gherkin em Inglês
|
19
15
|
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
16
|
+
|
17
|
+
# Mapeamento de PT → EN
|
20
18
|
GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
19
|
+
|
20
|
+
# Mapeamento de EN → PT
|
21
21
|
GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
22
|
+
|
23
|
+
# Todas as palavras-chave reconhecidas
|
22
24
|
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
23
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
|
24
31
|
def self.dividir_examples(raw)
|
25
32
|
raw.select { |l| l.strip.start_with?('|') }
|
26
33
|
end
|
27
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
|
28
43
|
def self.gerar_feature(input, override_path = nil)
|
29
44
|
modo = ENV['BDD_MODE']&.to_sym || :static
|
30
45
|
|
31
|
-
# IA: Lê o arquivo txt e passa para IA → aplica limpeza
|
32
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
|
33
48
|
raw_txt = File.read(input)
|
34
49
|
historia = {
|
35
50
|
idioma: 'pt',
|
@@ -50,15 +65,15 @@ module Bddgenx
|
|
50
65
|
tag: 'ia',
|
51
66
|
passos: GherkinCleaner.limpar(texto_gerado).lines.map(&:strip).reject(&:empty?)
|
52
67
|
}
|
53
|
-
|
54
68
|
else
|
55
|
-
#
|
69
|
+
# Modo estático: utiliza estrutura vinda do Parser ou de um hash diretamente
|
56
70
|
historia = input.is_a?(String) ? Parser.ler_historia(input) : input
|
57
71
|
end
|
58
72
|
|
59
73
|
idioma = historia[:idioma] || 'pt'
|
60
74
|
cont = 1
|
61
75
|
|
76
|
+
# Normaliza o nome base do arquivo
|
62
77
|
nome_base = historia[:quero]
|
63
78
|
.gsub(/[^a-z0-9]/i, '_')
|
64
79
|
.downcase
|
@@ -69,6 +84,7 @@ module Bddgenx
|
|
69
84
|
|
70
85
|
caminho = override_path || "features/#{nome_base}.feature"
|
71
86
|
|
87
|
+
# Define palavras-chave com base no idioma
|
72
88
|
palavras = {
|
73
89
|
feature: idioma == 'en' ? 'Feature' : 'Funcionalidade',
|
74
90
|
contexto: idioma == 'en' ? 'Background' : 'Contexto',
|
@@ -79,15 +95,13 @@ module Bddgenx
|
|
79
95
|
}
|
80
96
|
|
81
97
|
conteudo = <<~GHK
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
98
|
+
# language: #{idioma}
|
99
|
+
#{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
|
100
|
+
# #{historia[:como]}
|
101
|
+
# #{historia[:quero]}
|
102
|
+
# #{historia[:para]}
|
88
103
|
GHK
|
89
104
|
|
90
|
-
# Set para rastrear steps únicos
|
91
105
|
passos_unicos = Set.new
|
92
106
|
pt_map = GHERKIN_MAP_PT_EN
|
93
107
|
en_map = GHERKIN_MAP_EN_PT
|
@@ -100,7 +114,6 @@ module Bddgenx
|
|
100
114
|
next if passos.empty?
|
101
115
|
|
102
116
|
tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
|
103
|
-
|
104
117
|
conteudo << " #{tag_line}\n"
|
105
118
|
|
106
119
|
if exemplos.any?
|
@@ -137,11 +150,22 @@ module Bddgenx
|
|
137
150
|
[caminho, conteudo]
|
138
151
|
end
|
139
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
|
140
158
|
def self.path_para_feature(arquivo_txt)
|
141
159
|
nome = File.basename(arquivo_txt, '.txt')
|
142
160
|
File.join('features', "#{nome}.feature")
|
143
161
|
end
|
144
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]
|
145
169
|
def self.salvar_feature(caminho, conteudo)
|
146
170
|
FileUtils.mkdir_p(File.dirname(caminho))
|
147
171
|
File.write(caminho, conteudo)
|
@@ -4,16 +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'
|
15
|
-
require_relative 'ia/gemini_cliente'
|
16
|
-
require_relative 'utils/gherkin_cleaner'
|
7
|
+
require_relative '../../bddgenx'
|
17
8
|
|
18
9
|
module Bddgenx
|
19
10
|
# Ponto de entrada da gem: coordena todo o processo de geração BDD.
|
@@ -110,26 +101,15 @@ module Bddgenx
|
|
110
101
|
end
|
111
102
|
|
112
103
|
# Geração de feature
|
113
|
-
if modo == 'gemini'
|
114
|
-
puts "🤖 Gerando cenários com IA (
|
115
|
-
idioma = IA::GeminiCliente.detecta_idioma_arquivo(arquivo)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
sleep(0.4)
|
122
|
-
print "\r⏳ Aguardando resposta da IA#{'.' * (i + 1)} "
|
123
|
-
end
|
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)
|
124
112
|
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
113
|
if feature_text
|
134
114
|
feature_path = Generator.path_para_feature(arquivo)
|
135
115
|
feature_content = Bddgenx::GherkinCleaner.limpar(feature_text)
|
@@ -6,26 +6,45 @@
|
|
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
|
-
require 'set'
|
12
|
-
|
13
9
|
module Bddgenx
|
14
10
|
class StepsGenerator
|
11
|
+
# Palavras-chave Gherkin em Português
|
15
12
|
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
13
|
+
|
14
|
+
# Palavras-chave Gherkin em Inglês
|
16
15
|
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
16
|
+
|
17
|
+
# Conjunto de todas as palavras-chave suportadas
|
17
18
|
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
18
19
|
|
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.
|
25
|
+
#
|
19
26
|
def self.camelize(str)
|
20
27
|
partes = str.strip.split(/[^a-zA-Z0-9]+/)
|
21
28
|
partes.map.with_index { |palavra, i| i.zero? ? palavra.downcase : palavra.capitalize }.join
|
22
29
|
end
|
23
30
|
|
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.
|
41
|
+
#
|
24
42
|
def self.gerar_passos(feature_path)
|
25
43
|
raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
|
26
44
|
|
27
45
|
linhas = File.readlines(feature_path)
|
28
46
|
|
47
|
+
# Detecta o idioma com base na diretiva "# language:"
|
29
48
|
lang = if (m = linhas.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
|
30
49
|
m[/^#\s*language:\s*(\w+)/i, 1].downcase
|
31
50
|
else
|
@@ -35,6 +54,7 @@ module Bddgenx
|
|
35
54
|
pt_para_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
36
55
|
en_para_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
37
56
|
|
57
|
+
# Seleciona apenas linhas que começam com palavras-chave Gherkin
|
38
58
|
linhas_passos = linhas.map(&:strip).select do |linha|
|
39
59
|
ALL_KEYS.any? { |chave| linha.start_with?(chave + ' ') }
|
40
60
|
end
|
@@ -53,6 +73,7 @@ module Bddgenx
|
|
53
73
|
linhas_passos.each do |linha|
|
54
74
|
palavra_original, restante = linha.split(' ', 2)
|
55
75
|
|
76
|
+
# Tradução de palavras-chave se necessário
|
56
77
|
chave = case lang
|
57
78
|
when 'en' then pt_para_en[palavra_original] || palavra_original
|
58
79
|
else en_para_pt[palavra_original] || palavra_original
|
@@ -63,6 +84,7 @@ module Bddgenx
|
|
63
84
|
padrao = ''
|
64
85
|
tokens = []
|
65
86
|
|
87
|
+
# Analisa e parametriza o conteúdo dos passos
|
66
88
|
until scanner.eos?
|
67
89
|
if scanner.check(/"<([^>]+)>"/)
|
68
90
|
scanner.scan(/"<([^>]+)>"/)
|
@@ -87,7 +109,7 @@ module Bddgenx
|
|
87
109
|
|
88
110
|
padrao_seguro = padrao.gsub('"', '\\"')
|
89
111
|
|
90
|
-
# Impede
|
112
|
+
# Impede criação de métodos duplicados
|
91
113
|
next if passos_unicos.include?(padrao_seguro)
|
92
114
|
|
93
115
|
passos_unicos << padrao_seguro
|
@@ -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
|
@@ -1,15 +1,22 @@
|
|
1
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
2
|
|
8
3
|
module Bddgenx
|
9
4
|
module IA
|
5
|
+
##
|
6
|
+
# Cliente para interação com a API Gemini do Google para geração
|
7
|
+
# de conteúdo, aqui usado para criar cenários BDD no formato Gherkin.
|
8
|
+
#
|
10
9
|
class GeminiCliente
|
11
10
|
GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'.freeze
|
12
11
|
|
12
|
+
##
|
13
|
+
# Gera cenários BDD baseados em uma história, solicitando à API Gemini
|
14
|
+
# o retorno no formato Gherkin com palavras-chave no idioma desejado.
|
15
|
+
#
|
16
|
+
# @param historia [String] Texto base da história para gerar os cenários.
|
17
|
+
# @param idioma [String] Código do idioma, 'pt' por padrão.
|
18
|
+
# @return [String, nil] Cenários no formato Gherkin, ou nil em caso de erro.
|
19
|
+
#
|
13
20
|
def self.gerar_cenarios(historia, idioma = 'pt')
|
14
21
|
api_key = ENV['GEMINI_API_KEY']
|
15
22
|
|
@@ -37,7 +44,7 @@ module Bddgenx
|
|
37
44
|
|
38
45
|
keywords = idioma == 'en' ? keywords_en : keywords_pt
|
39
46
|
|
40
|
-
# Prompt base
|
47
|
+
# Prompt base que instrui a IA a gerar cenários Gherkin no idioma indicado
|
41
48
|
prompt_base = <<~PROMPT
|
42
49
|
Gere cenários BDD no formato Gherkin, usando as palavras-chave de estrutura no idioma "#{idioma}":
|
43
50
|
Feature: #{keywords[:feature]}
|
@@ -54,23 +61,25 @@ module Bddgenx
|
|
54
61
|
História:
|
55
62
|
#{historia}
|
56
63
|
PROMPT
|
64
|
+
|
57
65
|
unless api_key
|
58
66
|
warn "❌ API Key do Gemini não encontrada no .env (GEMINI_API_KEY)"
|
59
67
|
return nil
|
60
68
|
end
|
61
69
|
|
62
70
|
uri = URI("#{GEMINI_API_URL}?key=#{api_key}")
|
63
|
-
prompt = prompt_base % { historia: historia }
|
64
71
|
|
72
|
+
# Estrutura do corpo da requisição para a API Gemini
|
65
73
|
request_body = {
|
66
74
|
contents: [
|
67
75
|
{
|
68
76
|
role: "user",
|
69
|
-
parts: [{ text:
|
77
|
+
parts: [{ text: prompt_base }]
|
70
78
|
}
|
71
79
|
]
|
72
80
|
}
|
73
81
|
|
82
|
+
# Executa requisição POST para a API Gemini
|
74
83
|
response = Net::HTTP.post(uri, request_body.to_json, { "Content-Type" => "application/json" })
|
75
84
|
|
76
85
|
if response.is_a?(Net::HTTPSuccess)
|
@@ -84,11 +93,11 @@ module Bddgenx
|
|
84
93
|
|
85
94
|
texto_ia = json["candidates"].first.dig("content", "parts", 0, "text")
|
86
95
|
if texto_ia
|
87
|
-
#
|
96
|
+
# Limpeza e sanitização do texto para manter padrão Gherkin
|
88
97
|
texto_limpo = Bddgenx::GherkinCleaner.limpar(texto_ia)
|
89
98
|
Utils::StepCleaner.remover_steps_duplicados(texto_ia, idioma)
|
90
99
|
|
91
|
-
#
|
100
|
+
# Ajuste da diretiva de idioma na saída gerada
|
92
101
|
texto_limpo.sub!(/^# language: .*/, "# language: #{idioma}")
|
93
102
|
texto_limpo.prepend("# language: #{idioma}\n") unless texto_limpo.start_with?("# language:")
|
94
103
|
|
@@ -104,6 +113,12 @@ module Bddgenx
|
|
104
113
|
end
|
105
114
|
end
|
106
115
|
|
116
|
+
##
|
117
|
+
# Detecta o idioma do arquivo de feature pela linha "# language:".
|
118
|
+
#
|
119
|
+
# @param caminho_arquivo [String] Caminho do arquivo para detecção do idioma.
|
120
|
+
# @return [String] Código do idioma detectado (ex: 'pt'), padrão 'pt'.
|
121
|
+
#
|
107
122
|
def self.detecta_idioma_arquivo(caminho_arquivo)
|
108
123
|
return 'pt' unless File.exist?(caminho_arquivo)
|
109
124
|
|
@@ -113,7 +128,7 @@ module Bddgenx
|
|
113
128
|
end
|
114
129
|
end
|
115
130
|
|
116
|
-
'pt' # padrão
|
131
|
+
'pt' # idioma padrão caso não encontre
|
117
132
|
end
|
118
133
|
end
|
119
134
|
end
|
@@ -4,10 +4,6 @@
|
|
4
4
|
# Este arquivo define a classe Backup, responsável por criar cópias de segurança
|
5
5
|
# de arquivos .feature antes de serem sobrescritos.
|
6
6
|
# As cópias são salvas em 'reports/backup' com timestamp no nome.
|
7
|
-
|
8
|
-
require 'fileutils'
|
9
|
-
require 'time'
|
10
|
-
|
11
7
|
module Bddgenx
|
12
8
|
# Gerencia a criação de backups de arquivos .feature
|
13
9
|
class Backup
|
@@ -6,11 +6,6 @@
|
|
6
6
|
# preto e branco.
|
7
7
|
# Utiliza a gem Prawn para renderização de texto e tabelas.
|
8
8
|
|
9
|
-
require 'prawn'
|
10
|
-
require 'prawn/table'
|
11
|
-
require 'fileutils'
|
12
|
-
require_relative 'font_loader'
|
13
|
-
|
14
9
|
# Suprime aviso de internacionalização para fontes AFM internas
|
15
10
|
Prawn::Fonts::AFM.hide_m17n_warning = true
|
16
11
|
|
@@ -4,10 +4,6 @@
|
|
4
4
|
# Este arquivo define a classe Tracer, responsável por gerar e manter
|
5
5
|
# informações de rastreabilidade de cenários e passos em um arquivo CSV.
|
6
6
|
# Útil para auditoria e análise de cobertura de cenários gerados.
|
7
|
-
|
8
|
-
require 'csv'
|
9
|
-
require 'fileutils'
|
10
|
-
|
11
7
|
module Bddgenx
|
12
8
|
# Classe para adicionar registros de rastreabilidade a um relatório CSV.
|
13
9
|
class Tracer
|
@@ -4,10 +4,6 @@
|
|
4
4
|
# Este arquivo define a classe FontLoader, responsável por localizar e carregar
|
5
5
|
# famílias de fontes TrueType para uso com Prawn em geração de PDFs.
|
6
6
|
# Busca os arquivos de fonte no diretório assets/fonts dentro da gem.
|
7
|
-
|
8
|
-
require 'prawn'
|
9
|
-
require 'rubygems' # para Gem.loaded_specs se necessário
|
10
|
-
|
11
7
|
module Bddgenx
|
12
8
|
# Gerencia o carregamento de fontes TTF para os documentos PDF.
|
13
9
|
class FontLoader
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Bddgenx
|
2
|
+
class GherkinCleaner
|
3
|
+
# Método principal para limpar o texto Gherkin recebido.
|
4
|
+
# Executa uma sequência de operações para deixar o texto formatado e correto.
|
5
|
+
#
|
6
|
+
# Passos:
|
7
|
+
# - Remove blocos de código markdown (```).
|
8
|
+
# - Garante que exista somente uma linha # language: correta.
|
9
|
+
# - Corrige a indentação dos blocos Gherkin para o padrão esperado.
|
10
|
+
# - Remove espaços em branco no início/fim do texto.
|
11
|
+
#
|
12
|
+
# Retorna o texto limpo e formatado.
|
13
|
+
def self.limpar(texto)
|
14
|
+
texto = remover_blocos_markdown(texto)
|
15
|
+
texto = corrigir_language(texto)
|
16
|
+
texto = corrigir_indentacao(texto)
|
17
|
+
texto.strip
|
18
|
+
end
|
19
|
+
|
20
|
+
# Remove blocos markdown que usam as crases tripas (```).
|
21
|
+
# Muitas vezes a IA retorna os textos dentro desses blocos, que precisam ser limpos.
|
22
|
+
#
|
23
|
+
# Exemplo:
|
24
|
+
# ```gherkin
|
25
|
+
# Feature: Exemplo
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# Essa função remove as linhas contendo as crases, deixando só o conteúdo.
|
29
|
+
def self.remover_blocos_markdown(texto)
|
30
|
+
texto.gsub(/```[a-z]*\n?/i, '').gsub(/```/, '')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Garante que o arquivo contenha exatamente uma linha "# language: xx" no topo.
|
34
|
+
#
|
35
|
+
# Passos:
|
36
|
+
# - Procura a primeira ocorrência da linha de language no texto.
|
37
|
+
# - Remove todas as outras linhas duplicadas de language.
|
38
|
+
# - Se encontrar, move essa linha para o início do texto.
|
39
|
+
# - Se não encontrar, detecta o idioma do texto e adiciona a linha no topo.
|
40
|
+
#
|
41
|
+
# Isso evita erros de parsing em ferramentas BDD que exigem essa diretiva.
|
42
|
+
def self.corrigir_language(texto)
|
43
|
+
linhas = texto.lines
|
44
|
+
primeira_language = linhas.find { |linha| linha.strip.start_with?('# language:') }
|
45
|
+
|
46
|
+
# Remove todas as linhas duplicadas de language
|
47
|
+
linhas.reject! { |linha| linha.strip.start_with?('# language:') }
|
48
|
+
|
49
|
+
if primeira_language
|
50
|
+
# Insere a linha de language original no topo
|
51
|
+
linhas.unshift(primeira_language.strip + "\n")
|
52
|
+
else
|
53
|
+
# Se não existir, detecta o idioma e insere padrão
|
54
|
+
idioma = detectar_idioma(linhas.join)
|
55
|
+
linhas.unshift("# language: #{idioma}\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
linhas.join
|
59
|
+
end
|
60
|
+
|
61
|
+
# Detecta o idioma do conteúdo baseado nas palavras-chave Gherkin presentes.
|
62
|
+
#
|
63
|
+
# Retorna:
|
64
|
+
# - 'pt' se encontrar palavras-chave em português (Dado, Quando, Então, E).
|
65
|
+
# - 'en' se encontrar palavras-chave em inglês (Given, When, Then, And).
|
66
|
+
# - 'pt' como padrão se não detectar.
|
67
|
+
#
|
68
|
+
# Isso ajuda a definir a diretiva # language: corretamente.
|
69
|
+
def self.detectar_idioma(texto)
|
70
|
+
return 'pt' if texto =~ /Dado|Quando|Então|E /i
|
71
|
+
return 'en' if texto =~ /Given|When|Then|And /i
|
72
|
+
'pt' # padrão
|
73
|
+
end
|
74
|
+
|
75
|
+
# Corrige a indentação das linhas para seguir o padrão Gherkin:
|
76
|
+
#
|
77
|
+
# Feature e Funcionalidade no nível 0 (sem indentação).
|
78
|
+
# Scenario e Scenario Outline com 2 espaços.
|
79
|
+
# Passos (Given, When, Then, And e equivalentes PT) com 4 espaços.
|
80
|
+
# Tabelas (linhas que começam com '|') com 6 espaços.
|
81
|
+
#
|
82
|
+
# Outras linhas recebem indentação padrão de 2 espaços.
|
83
|
+
#
|
84
|
+
# Essa padronização melhora legibilidade e compatibilidade com parsers Gherkin.
|
85
|
+
def self.corrigir_indentacao(texto)
|
86
|
+
linhas = texto.lines.map do |linha|
|
87
|
+
if linha.strip.start_with?('Feature', 'Funcionalidade')
|
88
|
+
linha.strip + "\n"
|
89
|
+
elsif linha.strip.start_with?('Scenario', 'Cenário', 'Scenario Outline', 'Esquema do Cenário')
|
90
|
+
" #{linha.strip}\n"
|
91
|
+
elsif linha.strip.start_with?('Given', 'When', 'Then', 'And', 'Dado', 'Quando', 'Então', 'E')
|
92
|
+
" #{linha.strip}\n"
|
93
|
+
elsif linha.strip.start_with?('|')
|
94
|
+
" #{linha.strip}\n"
|
95
|
+
else
|
96
|
+
" #{linha.strip}\n"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
linhas.join
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Bddgenx
|
2
|
+
module Utils
|
3
|
+
class StepCleaner
|
4
|
+
# Remove passos duplicados em um texto de cenários BDD,
|
5
|
+
# levando em conta o idioma para identificar as keywords (Given, When, Then, And / Dado, Quando, Então, E)
|
6
|
+
#
|
7
|
+
# Parâmetros:
|
8
|
+
# - texto: string contendo o texto do cenário BDD
|
9
|
+
# - idioma: 'en' para inglês ou qualquer outro para português
|
10
|
+
#
|
11
|
+
# Retorna o texto com passos duplicados removidos, preservando a ordem original
|
12
|
+
def self.remover_steps_duplicados(texto, idioma)
|
13
|
+
# Define as keywords principais para o idioma
|
14
|
+
keywords = idioma == 'en' ? %w[Given When Then And] : %w[Dado Quando Então E]
|
15
|
+
|
16
|
+
# Conjunto para rastrear passos já vistos (versão canônica)
|
17
|
+
seen = Set.new
|
18
|
+
resultado = []
|
19
|
+
|
20
|
+
# Percorre linha a linha
|
21
|
+
texto.each_line do |linha|
|
22
|
+
# Verifica se a linha começa com uma das keywords
|
23
|
+
if keywords.any? { |kw| linha.strip.start_with?(kw) }
|
24
|
+
# Canonicaliza o passo para comparação sem variações irrelevantes
|
25
|
+
canonical = canonicalize_step(linha, keywords)
|
26
|
+
|
27
|
+
# Só adiciona se ainda não viu o passo canônico
|
28
|
+
unless seen.include?(canonical)
|
29
|
+
seen.add(canonical)
|
30
|
+
resultado << linha
|
31
|
+
end
|
32
|
+
else
|
33
|
+
# Linhas que não são passos são adicionadas normalmente
|
34
|
+
resultado << linha
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Retorna o texto reconstruído sem duplicatas
|
39
|
+
resultado.join
|
40
|
+
end
|
41
|
+
|
42
|
+
# Gera uma versão canônica (normalizada) do passo para facilitar
|
43
|
+
# a identificação de duplicatas mesmo com variações menores de texto.
|
44
|
+
#
|
45
|
+
# Exemplo: Dado "usuario" fez login e Dado <usuario> fez login
|
46
|
+
# gerarão o mesmo canonical para evitar repetição.
|
47
|
+
#
|
48
|
+
# Passos:
|
49
|
+
# - Remove a keyword (Given, When, etc) do começo
|
50
|
+
# - Substitui textos entre aspas, placeholders <> e números por <param>
|
51
|
+
# - Remove acentuação e pontuação para normalizar
|
52
|
+
# - Converte para minúsculas e remove espaços extras
|
53
|
+
#
|
54
|
+
# Parâmetros:
|
55
|
+
# - linha: string com o passo completo
|
56
|
+
# - keywords: array com as keywords para remoção
|
57
|
+
#
|
58
|
+
# Retorna uma string normalizada representando o passo
|
59
|
+
def self.canonicalize_step(linha, keywords)
|
60
|
+
texto = linha.dup.strip
|
61
|
+
|
62
|
+
# Remove a keyword do início, se existir
|
63
|
+
keywords.each do |kw|
|
64
|
+
texto.sub!(/^#{kw}\s+/i, '')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Substitui textos entre aspas, placeholders e números por <param>
|
68
|
+
texto.gsub!(/"[^"]*"|<[^>]*>|\b\d+\b/, '<param>')
|
69
|
+
|
70
|
+
# Remove acentos usando Unicode Normalization Form KD (decompõe caracteres)
|
71
|
+
texto = Unicode.normalize_KD(texto).gsub(/\p{Mn}/, '')
|
72
|
+
|
73
|
+
# Remove pontuação, deixando apenas letras, números, espaços e <>
|
74
|
+
texto.gsub!(/[^a-zA-Z0-9\s<>]/, '')
|
75
|
+
|
76
|
+
# Converte para minúsculas, remove espaços extras e retorna
|
77
|
+
texto.downcase.strip.squeeze(" ")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -4,7 +4,6 @@
|
|
4
4
|
# Este arquivo define a classe Validator, responsável por validar a estrutura
|
5
5
|
# de uma história antes de gerar cenários ou arquivos .feature.
|
6
6
|
# Verifica presença de cabeçalho obrigatório e integridade dos grupos de passos.
|
7
|
-
|
8
7
|
module Bddgenx
|
9
8
|
# Valida objetos de história garantindo que possuam campos e blocos corretos.
|
10
9
|
class Validator
|
data/lib/bddgenx.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
2
|
+
# Shebang para permitir execução direta do script no Linux/macOS, usando o interpretador Ruby do PATH
|
3
3
|
|
4
|
-
|
4
|
+
require_relative 'env'
|
5
|
+
# Carrega o arquivo 'env.rb' relativo a este script. Geralmente contém configurações e carregamento da gem Bddgenx.
|
6
|
+
|
7
|
+
## Evita warns na saida do console
|
8
|
+
Gem::Specification.reset
|
9
|
+
# Reseta as especificações das gems carregadas para evitar warnings no console, comum em alguns ambientes Ruby.
|
10
|
+
|
11
|
+
# Só executa o código abaixo quando este arquivo for o ponto de entrada (entrypoint) da aplicação,
|
12
|
+
# evitando que o código seja executado quando este arquivo for apenas requerido por outro arquivo.
|
5
13
|
if __FILE__ == $PROGRAM_NAME
|
6
14
|
Bddgenx::Runner.execute
|
15
|
+
# Invoca o método principal da gem Bddgenx para iniciar a execução da ferramenta.
|
7
16
|
end
|
data/lib/env.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# lib/bddgenx/env.rb
|
2
|
+
|
3
|
+
# Carregamento de bibliotecas padrão e gems externas usadas no projeto
|
4
|
+
|
5
|
+
require 'json' # Para manipulação de dados JSON
|
6
|
+
require 'net/http' # Para fazer requisições HTTP
|
7
|
+
require 'uri' # Para manipulação de URLs
|
8
|
+
require 'fileutils' # Para manipulação de arquivos e diretórios
|
9
|
+
require 'prawn' # Biblioteca para geração de PDFs
|
10
|
+
require 'prawn/table' # Suporte a tabelas no Prawn PDF
|
11
|
+
require 'prawn-svg' # Para incorporar SVG em PDFs com Prawn
|
12
|
+
require 'open3' # Para executar comandos externos com captura de saída
|
13
|
+
require 'faraday' # Cliente HTTP para Gemini API
|
14
|
+
require 'dotenv' # Para carregar variáveis de ambiente de arquivos .env
|
15
|
+
require 'unicode' # Para manipulação avançada de strings Unicode (ex: remoção de acentos)
|
16
|
+
require 'bigdecimal' # Para operações matemáticas precisas com decimais
|
17
|
+
|
18
|
+
# Configura o caminho base do projeto e carrega as gems definidas no Gemfile (se existir)
|
19
|
+
require 'bundler/setup' if File.exist?(File.expand_path('../../Gemfile', __FILE__))
|
20
|
+
|
21
|
+
# Carregamento dos módulos utilitários (helpers)
|
22
|
+
require_relative 'bddgenx/support/gherkin_cleaner' # Limpeza e normalização de textos Gherkin
|
23
|
+
require_relative 'bddgenx/support/remover_steps_duplicados' # Remoção de steps duplicados em features
|
24
|
+
require_relative 'bddgenx/support/validator' # Validação de dados e entrada
|
25
|
+
require_relative 'bddgenx/support/font_loader' # Carregamento de fontes para geração PDF
|
26
|
+
|
27
|
+
# Carregamento dos clientes para Integração com Inteligência Artificial
|
28
|
+
require_relative 'bddgenx/ia/gemini_cliente' # Cliente para API Gemini (Google)
|
29
|
+
require_relative 'bddgenx/ia/chatgtp_cliente' # Cliente para API ChatGPT (OpenAI)
|
30
|
+
|
31
|
+
# Carregamento dos geradores de BDD (features, steps e runner)
|
32
|
+
require_relative 'bddgenx/generators/generator' # Gerador principal de arquivos .feature
|
33
|
+
require_relative 'bddgenx/generators/steps_generator' # Gerador de arquivos steps.rb
|
34
|
+
require_relative 'bddgenx/generators/runner' # Classe responsável pela execução do processo de geração
|
35
|
+
|
36
|
+
# Parser do arquivo de entrada e versão da gem
|
37
|
+
require_relative 'parser' # Parser para interpretar arquivos de entrada
|
38
|
+
require_relative 'version' # Informação da versão da gem
|
39
|
+
|
40
|
+
# Relatórios e exportação
|
41
|
+
require_relative 'bddgenx/reports/pdf_exporter' # Exporta relatórios em PDF usando Prawn
|
42
|
+
require_relative 'bddgenx/reports/backup' # Mecanismo de backup dos arquivos gerados
|
43
|
+
require_relative 'bddgenx/reports/tracer' # Rastreabilidade dos processos
|
44
|
+
|
45
|
+
# Define variável de ambiente global para indicar que o ambiente BDDGENX está em modo desenvolvimento
|
46
|
+
ENV['BDDGENX_ENV'] = 'development'
|
@@ -4,9 +4,7 @@
|
|
4
4
|
# Este arquivo define a classe Parser, responsável por ler e interpretar
|
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
|
-
# Português e Inglês na marcação de idioma e blocos de exemplos
|
8
|
-
require_relative '../ia/gemini_cliente'
|
9
|
-
|
7
|
+
# Português e Inglês na marcação de idioma e blocos de exemplos
|
10
8
|
module Bddgenx
|
11
9
|
# Tipos de blocos reconhecidos na história (.txt), incluindo variações em Português
|
12
10
|
# e Inglês para blocos de exemplo.
|
data/lib/version.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Bddgenx
|
2
|
+
# Caminho absoluto para o arquivo VERSION, que fica 3 níveis acima deste arquivo:
|
3
|
+
# lib/bddgenx/version.rb → lib/bddgenx → lib → raiz do projeto
|
4
|
+
VERSION_FILE = File.expand_path("../../VERSION", __FILE__)
|
5
|
+
|
6
|
+
# Lê o conteúdo do arquivo VERSION para definir a constante VERSION
|
7
|
+
# Se o arquivo não existir, exibe um aviso e define o valor padrão "0.0.0"
|
8
|
+
VERSION = if File.exist?(VERSION_FILE)
|
9
|
+
File.read(VERSION_FILE).strip
|
10
|
+
else
|
11
|
+
warn "WARNING: VERSION file not found, defaulting to 0.0.0"
|
12
|
+
"0.0.0"
|
13
|
+
end
|
14
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bddgenx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Nascimento
|
@@ -52,6 +52,62 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.2.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ruby-openai
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '8.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '8.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: faraday
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.13.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.13.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dotenv
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.1'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: unicode
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.4'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.4'
|
55
111
|
description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
|
56
112
|
rastreabilidade e integração com CI/CD.
|
57
113
|
email:
|
@@ -70,20 +126,21 @@ files:
|
|
70
126
|
- lib/bddgenx/assets/fonts/DejaVuSansMono-BoldOblique.ttf
|
71
127
|
- lib/bddgenx/assets/fonts/DejaVuSansMono-Oblique.ttf
|
72
128
|
- lib/bddgenx/assets/fonts/DejaVuSansMono.ttf
|
73
|
-
- lib/bddgenx/generator.rb
|
129
|
+
- lib/bddgenx/generators/generator.rb
|
130
|
+
- lib/bddgenx/generators/runner.rb
|
131
|
+
- lib/bddgenx/generators/steps_generator.rb
|
132
|
+
- lib/bddgenx/ia/chatgtp_cliente.rb
|
74
133
|
- lib/bddgenx/ia/gemini_cliente.rb
|
75
|
-
- lib/bddgenx/
|
76
|
-
- lib/bddgenx/
|
77
|
-
- lib/bddgenx/
|
78
|
-
- lib/bddgenx/
|
79
|
-
- lib/bddgenx/
|
80
|
-
- lib/bddgenx/
|
81
|
-
- lib/bddgenx/
|
82
|
-
- lib/
|
83
|
-
- lib/
|
84
|
-
- lib/
|
85
|
-
- lib/bddgenx/utils/validator.rb
|
86
|
-
- lib/bddgenx/version.rb
|
134
|
+
- lib/bddgenx/reports/backup.rb
|
135
|
+
- lib/bddgenx/reports/pdf_exporter.rb
|
136
|
+
- lib/bddgenx/reports/tracer.rb
|
137
|
+
- lib/bddgenx/support/font_loader.rb
|
138
|
+
- lib/bddgenx/support/gherkin_cleaner.rb
|
139
|
+
- lib/bddgenx/support/remover_steps_duplicados.rb
|
140
|
+
- lib/bddgenx/support/validator.rb
|
141
|
+
- lib/env.rb
|
142
|
+
- lib/parser.rb
|
143
|
+
- lib/version.rb
|
87
144
|
homepage: https://github.com/David-Nascimento/bdd-generation
|
88
145
|
licenses:
|
89
146
|
- MIT
|
@@ -1,127 +0,0 @@
|
|
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
|
@@ -1,57 +0,0 @@
|
|
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
|
@@ -1,44 +0,0 @@
|
|
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
|
data/lib/bddgenx/version.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
module Bddgenx
|
2
|
-
# Sobe 3 níveis: lib/bddgenx/version.rb → bddgenx → lib → [raiz do projeto]
|
3
|
-
VERSION_FILE = File.expand_path("../../../VERSION", __FILE__)
|
4
|
-
|
5
|
-
VERSION = if File.exist?(VERSION_FILE)
|
6
|
-
File.read(VERSION_FILE).strip
|
7
|
-
else
|
8
|
-
warn "WARNING: VERSION file not found, defaulting to 0.0.0"
|
9
|
-
"0.0.0"
|
10
|
-
end
|
11
|
-
end
|