bddgenx 0.1.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 +7 -0
- data/README.md +161 -0
- data/bin/bddgenx +4 -0
- data/lib/bddgenx/backup.rb +16 -0
- data/lib/bddgenx/cli.rb +34 -0
- data/lib/bddgenx/generator.rb +123 -0
- data/lib/bddgenx/integrations/jira.rb +32 -0
- data/lib/bddgenx/integrations/testlink.rb +37 -0
- data/lib/bddgenx/parser.rb +52 -0
- data/lib/bddgenx/pdf_exporter.rb +29 -0
- data/lib/bddgenx/steps_generator.rb +107 -0
- data/lib/bddgenx/tracer.rb +32 -0
- data/lib/bddgenx/validator.rb +32 -0
- data/lib/bddgenx/version.rb +3 -0
- data/lib/bddgenx.rb +47 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ef4f9af959d68062c22da0b3cb716f9ac06ad41f10118cca1be47421d1bf7418
|
4
|
+
data.tar.gz: ad9885cb9a2baa14598a0278d3b7ff809bda5759058ef4828161e12c31485848
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91fbab9be46cabe7a27f15f8c47635c81d33801958373bceb40bf6ce0d925322f7889a5e79de9fc0dd7bb347971636639d99be7377afd05c15ef1d21cd8780cc
|
7
|
+
data.tar.gz: 71cb4f733fcf27a7d5c1fb8f0c1690f0322178a6b9e8c6813ecd91ebf302afe63eed0fc5f7bd4a4eb55d2b77b6d3e73df71b91c8e96e6b03f05c5719a4a7772f
|
data/README.md
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# 🧪 Gerador de BDD Automático em Ruby
|
2
|
+
|
3
|
+
Este projeto gera arquivos `.feature` (Gherkin) e `steps.rb` automaticamente a partir de arquivos `.txt` com histórias de usuário, seguindo padrões ISTQB, parametrização com `Examples` e integração com pipelines.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## 📂 Estrutura do Projeto
|
8
|
+
```txt
|
9
|
+
bdd_generator/
|
10
|
+
├── input/ # Arquivos .txt com histórias de usuário
|
11
|
+
├── features/ # Arquivos .feature gerados
|
12
|
+
├── steps/ # Arquivos com step definitions
|
13
|
+
├── output/
|
14
|
+
│ └── rastreabilidade.csv
|
15
|
+
├── backup/ # Versões antigas de features sobrescritas
|
16
|
+
├── lib/
|
17
|
+
│ ├── parser.rb
|
18
|
+
│ ├── generator.rb
|
19
|
+
│ ├── validator.rb
|
20
|
+
│ ├── steps_generator.rb
|
21
|
+
│ ├── tracer.rb
|
22
|
+
│ ├── backup.rb
|
23
|
+
├── main.rb
|
24
|
+
├── Rakefile
|
25
|
+
└── README.md
|
26
|
+
```
|
27
|
+
## ▶️ Como Executar
|
28
|
+
|
29
|
+
### 🔧 Requisitos
|
30
|
+
- Ruby 3.x
|
31
|
+
- `bundle install` (caso use gems como `prawn` ou `jira-ruby`)
|
32
|
+
|
33
|
+
### 🏁 Comando direto:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
ruby main.rb
|
37
|
+
```
|
38
|
+
|
39
|
+
🧱 Com Rake:
|
40
|
+
```bash
|
41
|
+
rake bddgen:gerar
|
42
|
+
```
|
43
|
+
|
44
|
+
📥 Como Escrever um .txt de Entrada
|
45
|
+
```txt
|
46
|
+
# language: pt
|
47
|
+
Como um cliente do e-commerce
|
48
|
+
Quero adicionar produtos ao carrinho
|
49
|
+
Para finalizar minha compra com praticidade
|
50
|
+
|
51
|
+
[CONTEXT]
|
52
|
+
Dado que estou logado na plataforma
|
53
|
+
E tenho produtos disponíveis
|
54
|
+
|
55
|
+
[REGRA]
|
56
|
+
O carrinho não deve permitir produtos fora de estoque
|
57
|
+
E o valor total deve refletir o desconto promocional
|
58
|
+
|
59
|
+
[SUCCESS]
|
60
|
+
Quando adiciono um produto ao carrinho
|
61
|
+
Então ele aparece na listagem do carrinho
|
62
|
+
|
63
|
+
[FAILURE]
|
64
|
+
Quando tento adicionar um produto esgotado
|
65
|
+
Então recebo uma mensagem de "produto indisponível"
|
66
|
+
|
67
|
+
[EXAMPLES]
|
68
|
+
| produto | quantidade | total esperado |
|
69
|
+
| Camiseta Azul | 2 | 100 |
|
70
|
+
| Tênis Branco | 1 | 250 |
|
71
|
+
|
72
|
+
[SUCCESS]
|
73
|
+
Quando adiciono "<produto>" com quantidade <quantidade>
|
74
|
+
Então vejo o total <total esperado>
|
75
|
+
```
|
76
|
+
✅ Blocos Suportados
|
77
|
+
[CONTEXT] – contexto comum
|
78
|
+
|
79
|
+
[SUCCESS] – cenário positivo
|
80
|
+
|
81
|
+
[FAILURE] – cenário negativo
|
82
|
+
|
83
|
+
[ERROR], [EXCEPTION], [PERFORMANCE], etc.
|
84
|
+
|
85
|
+
[REGRA] ou [RULE] – regras de negócio
|
86
|
+
|
87
|
+
[EXAMPLES] – tabela de dados para Scenario Outline
|
88
|
+
|
89
|
+
🧠 Saída esperada (feature)
|
90
|
+
```gherkin
|
91
|
+
# language: pt
|
92
|
+
Funcionalidade: adicionar produtos ao carrinho
|
93
|
+
|
94
|
+
Como um cliente do e-commerce
|
95
|
+
Quero adicionar produtos ao carrinho
|
96
|
+
Para finalizar minha compra com praticidade
|
97
|
+
|
98
|
+
Regra: O carrinho não deve permitir produtos fora de estoque
|
99
|
+
E o valor total deve refletir o desconto promocional
|
100
|
+
|
101
|
+
Contexto:
|
102
|
+
Dado que estou logado na plataforma
|
103
|
+
E tenho produtos disponíveis
|
104
|
+
|
105
|
+
@success
|
106
|
+
Cenário: Teste Positivo - adiciono um produto ao carrinho - ele aparece na listagem do carrinho
|
107
|
+
Quando adiciono um produto ao carrinho
|
108
|
+
Então ele aparece na listagem do carrinho
|
109
|
+
|
110
|
+
Esquema do Cenário: Gerado a partir de dados de exemplo
|
111
|
+
Quando adiciono "<produto>" com quantidade <quantidade>
|
112
|
+
Então vejo o total <total esperado>
|
113
|
+
|
114
|
+
Exemplos:
|
115
|
+
| produto | quantidade | total esperado |
|
116
|
+
| Camiseta Azul | 2 | 100 |
|
117
|
+
| Tênis Branco | 1 | 250 |
|
118
|
+
```
|
119
|
+
|
120
|
+
🧩 Step Definitions geradas
|
121
|
+
```ruby
|
122
|
+
Quando('adiciono "<produto>" com quantidade <quantidade>') do |produto, quantidade|
|
123
|
+
pending 'Implementar passo: adiciono "<produto>" com quantidade <quantidade>'
|
124
|
+
end
|
125
|
+
|
126
|
+
Então('vejo o total <total esperado>') do |total_esperado|
|
127
|
+
pending 'Implementar passo: vejo o total <total esperado>'
|
128
|
+
end
|
129
|
+
```
|
130
|
+
🧾 Rastreabilidade
|
131
|
+
- Gera automaticamente um CSV em output/rastreabilidade.csv com:
|
132
|
+
- Nome do cenário
|
133
|
+
- Tipo (SUCCESS, FAILURE, etc.)
|
134
|
+
- Caminho do .feature
|
135
|
+
- Origem do .txt
|
136
|
+
|
137
|
+
🔄 Backup
|
138
|
+
Toda vez que um .feature existente for sobrescrito, a versão anterior é salva em:
|
139
|
+
```
|
140
|
+
backup/
|
141
|
+
```
|
142
|
+
✅ Execução em CI/CD (GitHub Actions)
|
143
|
+
```yaml
|
144
|
+
jobs:
|
145
|
+
gerar_bdd:
|
146
|
+
runs-on: ubuntu-latest
|
147
|
+
steps:
|
148
|
+
- uses: actions/checkout@v3
|
149
|
+
- uses: ruby/setup-ruby@v1
|
150
|
+
with:
|
151
|
+
ruby-version: '3.2'
|
152
|
+
- run: ruby main.rb
|
153
|
+
```
|
154
|
+
|
155
|
+
👨💻 Autor
|
156
|
+
David Nascimento – Projeto de automação BDD com Ruby – 2025
|
157
|
+
```yaml
|
158
|
+
---
|
159
|
+
|
160
|
+
Pronto para copiar, colar ou subir no GitHub como `README.md`. Deseja que eu prepare um `.zip` com tudo funcionando como entrega final?
|
161
|
+
```
|
data/bin/bddgenx
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Backup
|
5
|
+
def self.salvar_versao_antiga(caminho)
|
6
|
+
return unless File.exist?(caminho)
|
7
|
+
|
8
|
+
FileUtils.mkdir_p("backup")
|
9
|
+
base = File.basename(caminho, ".feature")
|
10
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
11
|
+
destino = "backup/#{base}_#{timestamp}.feature"
|
12
|
+
|
13
|
+
FileUtils.cp(caminho, destino)
|
14
|
+
puts "📦 Backup criado: #{destino}"
|
15
|
+
end
|
16
|
+
end
|
data/lib/bddgenx/cli.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module CLI
|
2
|
+
def self.selecionar_arquivos_txt(diretorio)
|
3
|
+
arquivos = Dir.glob("#{diretorio}/*.txt")
|
4
|
+
|
5
|
+
if arquivos.empty?
|
6
|
+
puts "❌ Nenhum arquivo .txt encontrado no diretório '#{diretorio}'"
|
7
|
+
exit
|
8
|
+
end
|
9
|
+
|
10
|
+
arquivos
|
11
|
+
|
12
|
+
puts "📂 Arquivos disponíveis em '#{diretorio}':"
|
13
|
+
arquivos.each_with_index do |arquivo, i|
|
14
|
+
puts " #{i + 1}. #{File.basename(arquivo)}"
|
15
|
+
end
|
16
|
+
|
17
|
+
print "\nDigite os números dos arquivos que deseja processar (ex: 1,2,3 ou 'todos'): "
|
18
|
+
entrada = gets.chomp
|
19
|
+
|
20
|
+
selecionados = if entrada.downcase == 'todos'
|
21
|
+
arquivos
|
22
|
+
else
|
23
|
+
indices = entrada.split(',').map { |n| n.strip.to_i - 1 }
|
24
|
+
indices.map { |i| arquivos[i] }.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
if selecionados.empty?
|
28
|
+
puts "❌ Nenhum arquivo válido selecionado."
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
selecionados
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Generator
|
4
|
+
TIPOS_ISTQB = {
|
5
|
+
"SUCCESS" => "Teste Positivo",
|
6
|
+
"FAILURE" => "Teste Negativo",
|
7
|
+
"ERROR" => "Teste de Erro",
|
8
|
+
"EXCEPTION" => "Teste de Exceção",
|
9
|
+
"VALIDATION" => "Teste de Validação",
|
10
|
+
"PERMISSION" => "Teste de Permissão",
|
11
|
+
"EDGE_CASE" => "Teste de Limite",
|
12
|
+
"PERFORMANCE" => "Teste de Desempenho"
|
13
|
+
}
|
14
|
+
|
15
|
+
TIPOS_CENARIO = %w[
|
16
|
+
SUCCESS FAILURE ERROR EXCEPTION
|
17
|
+
VALIDATION PERMISSION EDGE_CASE PERFORMANCE
|
18
|
+
]
|
19
|
+
|
20
|
+
|
21
|
+
def self.gerar_feature(historia)
|
22
|
+
idioma = historia[:idioma] || 'pt'
|
23
|
+
|
24
|
+
# Define os conectores de acordo com o idioma
|
25
|
+
palavras = {
|
26
|
+
contexto: idioma == 'en' ? 'Background' : 'Contexto',
|
27
|
+
cenario: idioma == 'en' ? 'Scenario' : 'Cenário',
|
28
|
+
esquema: idioma == 'en' ? 'Scenario Outline' : 'Esquema do Cenário',
|
29
|
+
exemplos: idioma == 'en' ? 'Examples' : 'Exemplos',
|
30
|
+
regra: idioma == 'en' ? 'Rule' : 'Regra'
|
31
|
+
}
|
32
|
+
|
33
|
+
nome_base = historia[:quero].gsub(/[^a-zA-Z0-9]/, '_').downcase
|
34
|
+
caminho = "features/#{nome_base}.feature"
|
35
|
+
|
36
|
+
conteudo = <<~HEADER
|
37
|
+
# language: #{idioma}
|
38
|
+
Funcionalidade: #{historia[:quero].sub(/^Quero/, '').strip}
|
39
|
+
|
40
|
+
#{historia[:como]}
|
41
|
+
#{historia[:quero]}
|
42
|
+
#{historia[:para]}
|
43
|
+
|
44
|
+
HEADER
|
45
|
+
|
46
|
+
# Regras
|
47
|
+
if historia[:regras]&.any?
|
48
|
+
conteudo += " #{palavras[:regra]}: #{historia[:regras].first}\n"
|
49
|
+
historia[:regras][1..].each do |linha|
|
50
|
+
conteudo += " #{linha}\n"
|
51
|
+
end
|
52
|
+
conteudo += "\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Contexto
|
57
|
+
if historia[:blocos]["CONTEXT"]&.any?
|
58
|
+
conteudo += " #{palavras[:contexto]}:\n"
|
59
|
+
historia[:blocos]["CONTEXT"].each { |p| conteudo += " #{p}\n" }
|
60
|
+
conteudo += "\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Cenários
|
64
|
+
TIPOS_CENARIO.each do |tipo|
|
65
|
+
passos = historia[:blocos][tipo]
|
66
|
+
next unless passos&.any?
|
67
|
+
|
68
|
+
nome_teste = TIPOS_ISTQB[tipo] || palavras[:cenario]
|
69
|
+
contexto = passos.first&.gsub(/^(Dado que|Given|Quando|When|Então|Then|E|And)/, '')&.strip || "Condição"
|
70
|
+
resultado = passos.last&.gsub(/^(Então|Then|E|And)/, '')&.strip || "Resultado"
|
71
|
+
nome_cenario = "#{nome_teste} - #{contexto} - #{resultado}"
|
72
|
+
|
73
|
+
conteudo += " @#{tipo.downcase}\n"
|
74
|
+
conteudo += " #{palavras[:cenario]}: #{nome_cenario}\n"
|
75
|
+
passos.each { |p| conteudo += " #{p}\n" }
|
76
|
+
conteudo += "\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Esquema do Cenário com Exemplos
|
80
|
+
if historia[:blocos]["EXAMPLES"]&.any?
|
81
|
+
exemplos_bruto = historia[:blocos]["EXAMPLES"]
|
82
|
+
cabecalho = exemplos_bruto.first.gsub('|', '').split.map(&:strip)
|
83
|
+
linhas = exemplos_bruto[1..]
|
84
|
+
|
85
|
+
# Procura qualquer cenário que use parâmetros
|
86
|
+
tipo_cenario = historia[:blocos].keys.find { |k| historia[:blocos][k].any? { |l| l.include?('<') } }
|
87
|
+
passos_exemplo = tipo_cenario ? historia[:blocos][tipo_cenario].select { |l| l.include?('<') } : []
|
88
|
+
|
89
|
+
if idioma == 'en'
|
90
|
+
conteudo += " Scenario Outline: Generated scenario with data\n"
|
91
|
+
else
|
92
|
+
conteudo += " Esquema do Cenário: Gerado a partir de dados de exemplo\n"
|
93
|
+
end
|
94
|
+
|
95
|
+
passos_exemplo.each do |passo|
|
96
|
+
conteudo += " #{passo}\n"
|
97
|
+
end
|
98
|
+
|
99
|
+
conteudo += "\n"
|
100
|
+
conteudo += idioma == 'en' ? " Examples:\n" : " Exemplos:\n"
|
101
|
+
conteudo += " #{exemplos_bruto.first}\n"
|
102
|
+
linhas.each { |linha| conteudo += " #{linha}\n" }
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
[caminho, conteudo]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Salva o arquivo .feature gerado
|
112
|
+
# Retorna true se o arquivo foi salvo com sucesso, false caso contrário
|
113
|
+
def self.salvar_feature(caminho, conteudo)
|
114
|
+
if conteudo.strip.empty?
|
115
|
+
puts "⚠️ Nenhum conteúdo gerado para: #{caminho} (ignorado)"
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
|
119
|
+
File.write(caminho, conteudo)
|
120
|
+
puts "✅ Arquivo .feature gerado: #{caminho}"
|
121
|
+
true
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'jira-ruby'
|
2
|
+
|
3
|
+
module Bddgen
|
4
|
+
module Integrations
|
5
|
+
class Jira
|
6
|
+
def initialize(options = {})
|
7
|
+
@client = JIRA::Client.new(
|
8
|
+
username: options[:username],
|
9
|
+
password: options[:api_token],
|
10
|
+
site: options[:site],
|
11
|
+
context_path: '',
|
12
|
+
auth_type: :basic
|
13
|
+
)
|
14
|
+
@project_key = options[:project_key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def enviar_cenario(titulo, descricao)
|
18
|
+
issue = {
|
19
|
+
fields: {
|
20
|
+
project: { key: @project_key },
|
21
|
+
summary: titulo,
|
22
|
+
description: descricao,
|
23
|
+
issuetype: { name: "Task" }
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
@client.Issue.build.save(issue)
|
28
|
+
puts "✅ Cenário enviado para Jira: #{titulo}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'xmlrpc/client'
|
2
|
+
|
3
|
+
module Bddgen
|
4
|
+
module Integrations
|
5
|
+
class TestLink
|
6
|
+
def initialize(api_key, url)
|
7
|
+
@server = XMLRPC::Client.new2(url)
|
8
|
+
@key = api_key
|
9
|
+
end
|
10
|
+
|
11
|
+
def criar_caso_teste(plan_id, titulo, passos)
|
12
|
+
steps_formated = passos.map.with_index(1) do |step, i|
|
13
|
+
{
|
14
|
+
step_number: i,
|
15
|
+
actions: step,
|
16
|
+
expected_results: '',
|
17
|
+
execution_type: 1
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
params = {
|
22
|
+
devKey: @key,
|
23
|
+
testprojectid: 1,
|
24
|
+
testsuiteid: plan_id,
|
25
|
+
testcasename: titulo,
|
26
|
+
steps: steps_formated
|
27
|
+
}
|
28
|
+
|
29
|
+
response = @server.call('tl.createTestCase', params)
|
30
|
+
puts "✅ Teste enviado ao TestLink: #{titulo}"
|
31
|
+
response
|
32
|
+
rescue => e
|
33
|
+
puts "❌ Erro ao criar caso no TestLink: #{e.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Parser
|
2
|
+
TIPOS_BLOCOS = %w[
|
3
|
+
CONTEXT SUCCESS FAILURE ERROR EXCEPTION
|
4
|
+
VALIDATION PERMISSION EDGE_CASE PERFORMANCE
|
5
|
+
EXAMPLES REGRA RULE
|
6
|
+
].freeze
|
7
|
+
|
8
|
+
def self.ler_historia(caminho_arquivo)
|
9
|
+
linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:strip).reject(&:empty?)
|
10
|
+
|
11
|
+
# Detecta idioma
|
12
|
+
idioma = linhas.first.downcase.include?('# lang: en') ? 'en' : 'pt'
|
13
|
+
linhas.shift if linhas.first.downcase.start_with?('#')
|
14
|
+
|
15
|
+
# Ignora linhas que sejam blocos ou comentários até encontrar Como/Quero/Para
|
16
|
+
cabecalho = []
|
17
|
+
until linhas.empty?
|
18
|
+
linha = linhas.shift
|
19
|
+
break if linha.start_with?("Como", "As") # início da história em pt ou en
|
20
|
+
end
|
21
|
+
como = linha
|
22
|
+
quero = linhas.shift
|
23
|
+
para = linhas.shift
|
24
|
+
|
25
|
+
historia = {
|
26
|
+
como: como,
|
27
|
+
quero: quero,
|
28
|
+
para: para,
|
29
|
+
blocos: Hash.new { |h, k| h[k] = [] },
|
30
|
+
regras: [],
|
31
|
+
arquivo_origem: caminho_arquivo,
|
32
|
+
idioma: idioma
|
33
|
+
}
|
34
|
+
|
35
|
+
tipo_atual = nil
|
36
|
+
|
37
|
+
linhas.each do |linha|
|
38
|
+
if linha.match?(/^\[(#{TIPOS_BLOCOS.join('|')})\]$/)
|
39
|
+
tipo_atual = linha.gsub(/[\[\]]/, '')
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
if %w[REGRA RULE].include?(tipo_atual)
|
44
|
+
historia[:regras] << linha
|
45
|
+
else
|
46
|
+
historia[:blocos][tipo_atual] << linha if tipo_atual
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
historia
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Bddgen
|
5
|
+
class PDFExporter
|
6
|
+
def self.exportar_todos
|
7
|
+
FileUtils.mkdir_p('pdf')
|
8
|
+
|
9
|
+
Dir.glob('features/*.feature').each do |feature_file|
|
10
|
+
nome = File.basename(feature_file, '.feature')
|
11
|
+
destino = "pdf/#{nome}.pdf"
|
12
|
+
|
13
|
+
exportar_arquivo(feature_file, destino)
|
14
|
+
puts "📄 PDF gerado: #{destino}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.exportar_arquivo(origem, destino)
|
19
|
+
conteudo = File.read(origem, encoding: 'utf-8')
|
20
|
+
|
21
|
+
Prawn::Document.generate(destino) do |pdf|
|
22
|
+
pdf.font_size 10
|
23
|
+
pdf.text "Arquivo: #{File.basename(origem)}", style: :bold, size: 14
|
24
|
+
pdf.move_down 10
|
25
|
+
pdf.text conteudo, size: 10
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module StepsGenerator
|
4
|
+
PADROES = {
|
5
|
+
'pt' => %w[Dado Quando Então E],
|
6
|
+
'en' => %w[Given When Then And]
|
7
|
+
}
|
8
|
+
|
9
|
+
TIPOS_BLOCOS = %w[
|
10
|
+
CONTEXT SUCCESS FAILURE ERROR EXCEPTION
|
11
|
+
VALIDATION PERMISSION EDGE_CASE PERFORMANCE
|
12
|
+
EXAMPLES REGRA RULE
|
13
|
+
]
|
14
|
+
|
15
|
+
def self.gerar_passos(historia, nome_arquivo_feature)
|
16
|
+
idioma = historia[:idioma] || 'pt'
|
17
|
+
conectores = PADROES[idioma]
|
18
|
+
passos_gerados = []
|
19
|
+
|
20
|
+
# Detectar parâmetros e tipos a partir do bloco EXAMPLES
|
21
|
+
exemplos = extrair_exemplos(historia[:blocos]["EXAMPLES"])
|
22
|
+
|
23
|
+
TIPOS_BLOCOS.each do |tipo|
|
24
|
+
blocos = tipo == "REGRA" || tipo == "RULE" ? historia[:regras] : historia[:blocos][tipo]
|
25
|
+
next unless blocos.is_a?(Array)
|
26
|
+
|
27
|
+
blocos.each do |linha|
|
28
|
+
next if tipo == "EXAMPLES" && linha.strip.start_with?("|") # ignora linhas de tabela
|
29
|
+
|
30
|
+
conector = conectores.find { |c| linha.strip.start_with?(c) }
|
31
|
+
next unless conector
|
32
|
+
|
33
|
+
corpo = linha.strip.sub(/^#{conector}/, '').strip
|
34
|
+
corpo_parametrizado = substituir_parametros(corpo, exemplos)
|
35
|
+
|
36
|
+
chave = { conector: conector, raw: corpo, param: corpo_parametrizado }
|
37
|
+
passos_gerados << chave unless passos_gerados.any? { |p| p[:param] == corpo_parametrizado }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if passos_gerados.empty?
|
42
|
+
puts "⚠️ Nenhum passo detectado em: #{nome_arquivo_feature} (arquivo não gerado)"
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
|
46
|
+
nome_base = File.basename(nome_arquivo_feature, '.feature')
|
47
|
+
caminho = "steps/#{nome_base}_steps.rb"
|
48
|
+
FileUtils.mkdir_p(File.dirname(caminho))
|
49
|
+
|
50
|
+
comentario = "# Step definitions para #{File.basename(nome_arquivo_feature)}"
|
51
|
+
comentario += idioma == 'en' ? " (English)" : " (Português)"
|
52
|
+
conteudo = "#{comentario}\n\n"
|
53
|
+
|
54
|
+
passos_gerados.each do |passo|
|
55
|
+
# Extrai nomes dos parâmetros entre < >
|
56
|
+
parametros = passo[:raw].scan(/<([^>]+)>/).flatten.map(&:strip)
|
57
|
+
param_list = parametros.map { |p| p.gsub(' ', '_') }.join(', ')
|
58
|
+
|
59
|
+
conteudo += <<~STEP
|
60
|
+
#{passo[:conector]}('#{passo[:param]}') do#{param_list.empty? ? '' : " |#{param_list}|" }
|
61
|
+
pending '#{idioma == 'en' ? 'Implement step' : 'Implementar passo'}: #{passo[:raw]}'
|
62
|
+
end
|
63
|
+
|
64
|
+
STEP
|
65
|
+
end
|
66
|
+
|
67
|
+
FileUtils.mkdir_p("steps")
|
68
|
+
File.write(caminho, conteudo)
|
69
|
+
puts "✅ Step definitions gerados: #{caminho}"
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.substituir_parametros(texto, exemplos)
|
74
|
+
texto.gsub(/<([^>]+)>/) do |_match|
|
75
|
+
nome = $1.strip
|
76
|
+
tipo = detectar_tipo_param(nome, exemplos)
|
77
|
+
"{#{tipo}}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.detectar_tipo_param(nome_coluna, exemplos)
|
82
|
+
return 'string' unless exemplos && exemplos[:cabecalho].include?(nome_coluna)
|
83
|
+
|
84
|
+
idx = exemplos[:cabecalho].index(nome_coluna)
|
85
|
+
valores = exemplos[:linhas].map { |l| l[idx] }
|
86
|
+
|
87
|
+
if valores.all? { |v| v.match?(/^\d+$/) }
|
88
|
+
'int'
|
89
|
+
elsif valores.all? { |v| v.match?(/^\d+\.\d+$/) }
|
90
|
+
'float'
|
91
|
+
else
|
92
|
+
'string'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.extrair_exemplos(bloco)
|
97
|
+
return nil unless bloco&.any?
|
98
|
+
|
99
|
+
linhas = bloco.map(&:strip)
|
100
|
+
cabecalho = linhas.first.gsub('|', '').split.map(&:strip)
|
101
|
+
dados = linhas[1..].map { |linha| linha.gsub('|', '').split.map(&:strip) }
|
102
|
+
|
103
|
+
{ cabecalho: cabecalho, linhas: dados }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Tracer
|
5
|
+
ARQUIVO = 'output/rastreabilidade.csv'
|
6
|
+
|
7
|
+
def self.adicionar_entrada(historia, caminho_arquivo)
|
8
|
+
FileUtils.mkdir_p("output")
|
9
|
+
|
10
|
+
CSV.open(ARQUIVO, File.exist?(ARQUIVO) ? 'a' : 'w', col_sep: ';') do |csv|
|
11
|
+
unless File.exist?(ARQUIVO)
|
12
|
+
csv << ["Funcionalidade", "Tipo de Teste", "Nome do Cenário", "Arquivo .feature"]
|
13
|
+
end
|
14
|
+
|
15
|
+
historia[:blocos].each do |tipo, passos|
|
16
|
+
next if tipo == "CONTEXT" || tipo == "EXAMPLES" || passos.empty?
|
17
|
+
|
18
|
+
tipo_istqb = tipo.capitalize.gsub('_', ' ').capitalize
|
19
|
+
contexto = passos.first&.gsub(/^(Dado que|Quando|Então|E)/, '')&.strip || "Condição"
|
20
|
+
resultado = passos.last&.gsub(/^(Então|E)/, '')&.strip || "Resultado"
|
21
|
+
nome_cenario = "#{tipo_istqb} - #{contexto} - #{resultado}"
|
22
|
+
|
23
|
+
csv << [
|
24
|
+
historia[:quero].sub(/^Quero/, '').strip,
|
25
|
+
tipo_istqb,
|
26
|
+
nome_cenario,
|
27
|
+
caminho_arquivo
|
28
|
+
]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Validator
|
2
|
+
TIPOS_CENARIO = %w[
|
3
|
+
SUCCESS
|
4
|
+
FAILURE
|
5
|
+
ERROR
|
6
|
+
EXCEPTION
|
7
|
+
VALIDATION
|
8
|
+
PERMISSION
|
9
|
+
EDGE_CASE
|
10
|
+
PERFORMANCE
|
11
|
+
]
|
12
|
+
|
13
|
+
def self.validar(historia)
|
14
|
+
valido = true
|
15
|
+
|
16
|
+
if historia[:como].to_s.strip.empty? ||
|
17
|
+
historia[:quero].to_s.strip.empty? ||
|
18
|
+
historia[:para].to_s.strip.empty?
|
19
|
+
puts "❌ História incompleta: 'Como', 'Quero' ou 'Para' está faltando."
|
20
|
+
valido = false
|
21
|
+
end
|
22
|
+
|
23
|
+
cenarios_presentes = historia[:blocos].keys & TIPOS_CENARIO
|
24
|
+
valido ||= historia[:blocos]["CONTEXT"]&.any? || historia[:regras]&.any?
|
25
|
+
if cenarios_presentes.empty? && !valido
|
26
|
+
puts "❌ Nenhum conteúdo válido detectado (cenários, contexto ou regras)."
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
|
30
|
+
valido
|
31
|
+
end
|
32
|
+
end
|
data/lib/bddgenx.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative "bddgen/version"
|
2
|
+
require_relative "bddgen/cli"
|
3
|
+
require_relative "bddgen/parser"
|
4
|
+
require_relative "bddgen/validator"
|
5
|
+
require_relative "bddgen/generator"
|
6
|
+
require_relative "bddgen/steps_generator"
|
7
|
+
require_relative "bddgen/tracer"
|
8
|
+
require_relative "bddgen/backup"
|
9
|
+
require_relative 'bddgen/pdf_exporter'
|
10
|
+
|
11
|
+
cont_total = 0
|
12
|
+
cont_features = 0
|
13
|
+
cont_steps = 0
|
14
|
+
cont_ignorados = 0
|
15
|
+
|
16
|
+
# Exibe menu inicial e pergunta quais arquivos processar
|
17
|
+
arquivos = CLI.selecionar_arquivos_txt('input')
|
18
|
+
|
19
|
+
arquivos.each do |arquivo_path|
|
20
|
+
puts "\n🔍 Processando: #{arquivo_path}"
|
21
|
+
|
22
|
+
historia = Parser.ler_historia(arquivo_path)
|
23
|
+
|
24
|
+
unless Validator.validar(historia)
|
25
|
+
cont_ignorados += 1
|
26
|
+
puts "❌ Arquivo inválido: #{arquivo_path}"
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
nome_feature, conteudo_feature = Generator.gerar_feature(historia)
|
31
|
+
|
32
|
+
Backup.salvar_versao_antiga(nome_feature)
|
33
|
+
cont_features += 1 if Generator.salvar_feature(nome_feature, conteudo_feature)
|
34
|
+
cont_steps += 1 if StepsGenerator.gerar_passos(historia, nome_feature)
|
35
|
+
|
36
|
+
|
37
|
+
Tracer.adicionar_entrada(historia, nome_feature)
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "\n✅ Processamento finalizado. Arquivos gerados em: features/, steps/, output/"
|
41
|
+
puts "🔄 Versões antigas salvas em: backup/"
|
42
|
+
|
43
|
+
puts "\n✅ Processamento finalizado:"
|
44
|
+
puts "- Arquivos processados: #{cont_total}"
|
45
|
+
puts "- Features geradas: #{cont_features}"
|
46
|
+
puts "- Steps gerados: #{cont_steps}"
|
47
|
+
puts "- Ignorados: #{cont_ignorados}"
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bddgenx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Nascimento
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
|
14
|
+
rastreabilidade e integração com CI/CD.
|
15
|
+
email:
|
16
|
+
- halison700@gmail.com
|
17
|
+
executables:
|
18
|
+
- bddgenx
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- README.md
|
23
|
+
- bin/bddgenx
|
24
|
+
- lib/bddgenx.rb
|
25
|
+
- lib/bddgenx/backup.rb
|
26
|
+
- lib/bddgenx/cli.rb
|
27
|
+
- lib/bddgenx/generator.rb
|
28
|
+
- lib/bddgenx/integrations/jira.rb
|
29
|
+
- lib/bddgenx/integrations/testlink.rb
|
30
|
+
- lib/bddgenx/parser.rb
|
31
|
+
- lib/bddgenx/pdf_exporter.rb
|
32
|
+
- lib/bddgenx/steps_generator.rb
|
33
|
+
- lib/bddgenx/tracer.rb
|
34
|
+
- lib/bddgenx/validator.rb
|
35
|
+
- lib/bddgenx/version.rb
|
36
|
+
homepage: https://github.com/David-Nascimento/bdd-generation
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 3.x
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubygems_version: 3.3.27
|
56
|
+
signing_key:
|
57
|
+
specification_version: 4
|
58
|
+
summary: Geração automática de BDD a partir de histórias de usuário
|
59
|
+
test_files: []
|