bddgenx 0.1.43 → 0.1.44
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 +61 -129
- data/Rakefile +41 -40
- data/VERSION +1 -1
- data/lib/bddgenx/generator.rb +86 -41
- data/lib/bddgenx/runner.rb +52 -21
- data/lib/bddgenx/steps_generator.rb +87 -58
- data/lib/bddgenx/utils/backup.rb +19 -4
- data/lib/bddgenx/utils/fontLoader.rb +24 -6
- data/lib/bddgenx/utils/parser.rb +49 -10
- data/lib/bddgenx/utils/pdf_exporter.rb +51 -18
- data/lib/bddgenx/utils/tracer.rb +31 -3
- data/lib/bddgenx/utils/validator.rb +29 -3
- metadata +29 -2
- data/lib/bddgenx/utils/tipo_param.rb +0 -16
data/lib/bddgenx/runner.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
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
|
+
|
2
8
|
require 'fileutils'
|
3
9
|
require_relative 'utils/parser'
|
4
10
|
require_relative 'generator'
|
@@ -8,17 +14,23 @@ require_relative 'utils/validator'
|
|
8
14
|
require_relative 'utils/backup'
|
9
15
|
|
10
16
|
module Bddgenx
|
17
|
+
# Ponto de entrada da gem: coordena todo o processo de geração BDD.
|
11
18
|
class Runner
|
12
|
-
#
|
19
|
+
# Seleciona arquivos de entrada para processamento.
|
20
|
+
# Se houver argumentos em ARGV, usa-os como nomes de arquivos .txt;
|
21
|
+
# caso contrário, exibe prompt interativo para escolha.
|
22
|
+
#
|
23
|
+
# @param input_dir [String] Diretório onde estão os arquivos .txt de histórias
|
24
|
+
# @return [Array<String>] Lista de caminhos para os arquivos a serem processados
|
13
25
|
def self.choose_files(input_dir)
|
14
|
-
|
15
|
-
selecionar_arquivos_txt(input_dir)
|
16
|
-
else
|
17
|
-
choose_input(input_dir)
|
18
|
-
end
|
26
|
+
ARGV.any? ? selecionar_arquivos_txt(input_dir) : choose_input(input_dir)
|
19
27
|
end
|
20
28
|
|
21
|
-
#
|
29
|
+
# Mapeia ARGV para paths de arquivos .txt em input_dir.
|
30
|
+
# Adiciona extensão '.txt' se necessário e filtra arquivos inexistentes.
|
31
|
+
#
|
32
|
+
# @param input_dir [String] Diretório de entrada
|
33
|
+
# @return [Array<String>] Caminhos válidos para processamento
|
22
34
|
def self.selecionar_arquivos_txt(input_dir)
|
23
35
|
ARGV.map do |arg|
|
24
36
|
nome = arg.end_with?('.txt') ? arg : "#{arg}.txt"
|
@@ -31,26 +43,42 @@ module Bddgenx
|
|
31
43
|
end.compact
|
32
44
|
end
|
33
45
|
|
34
|
-
#
|
46
|
+
# Exibe prompt interativo para o usuário escolher qual arquivo processar
|
47
|
+
# entre todos os .txt disponíveis em input_dir.
|
48
|
+
#
|
49
|
+
# @param input_dir [String] Diretório de entrada
|
50
|
+
# @exit [1] Se nenhum arquivo for encontrado ou escolha inválida
|
51
|
+
# @return [Array<String>] Um único arquivo escolhido ou todos se ENTER
|
35
52
|
def self.choose_input(input_dir)
|
36
53
|
files = Dir.glob(File.join(input_dir, '*.txt'))
|
37
54
|
if files.empty?
|
38
|
-
warn "❌ Não há arquivos .txt no diretório #{input_dir}"
|
39
|
-
exit 1
|
55
|
+
warn "❌ Não há arquivos .txt no diretório #{input_dir}"; exit 1
|
40
56
|
end
|
41
57
|
|
42
58
|
puts "Selecione o arquivo de história para processar:"
|
43
|
-
files.each_with_index { |f,i| puts "#{i+1}. #{File.basename(f)}" }
|
59
|
+
files.each_with_index { |f, i| puts "#{i+1}. #{File.basename(f)}" }
|
44
60
|
print "Digite o número correspondente (ou ENTER para todos): "
|
45
61
|
choice = STDIN.gets.chomp
|
62
|
+
|
46
63
|
return files if choice.empty?
|
47
64
|
idx = choice.to_i - 1
|
48
|
-
unless idx.between?(0, files.size-1)
|
65
|
+
unless idx.between?(0, files.size - 1)
|
49
66
|
warn "❌ Escolha inválida."; exit 1
|
50
67
|
end
|
51
68
|
[files[idx]]
|
52
69
|
end
|
53
70
|
|
71
|
+
# Executa todo o fluxo de geração BDD.
|
72
|
+
# - Cria pasta 'input' se não existir
|
73
|
+
# - Seleciona arquivos de histórias
|
74
|
+
# - Para cada arquivo:
|
75
|
+
# - Lê e valida a história
|
76
|
+
# - Gera arquivo .feature e salva backup da versão anterior
|
77
|
+
# - Gera definitions de steps
|
78
|
+
# - Exporta PDFs novos via PDFExporter
|
79
|
+
# - Exibe resumo final com estatísticas
|
80
|
+
#
|
81
|
+
# @return [void]
|
54
82
|
def self.execute
|
55
83
|
input_dir = 'input'
|
56
84
|
Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
|
@@ -60,6 +88,7 @@ module Bddgenx
|
|
60
88
|
warn "❌ Nenhum arquivo de história para processar."; exit 1
|
61
89
|
end
|
62
90
|
|
91
|
+
# Inicializa contadores
|
63
92
|
total = features = steps = ignored = 0
|
64
93
|
skipped_steps = []
|
65
94
|
generated_pdfs = []
|
@@ -71,29 +100,31 @@ module Bddgenx
|
|
71
100
|
|
72
101
|
historia = Parser.ler_historia(arquivo)
|
73
102
|
unless Validator.validar(historia)
|
74
|
-
ignored += 1
|
103
|
+
ignored += 1
|
104
|
+
puts "❌ História inválida: #{arquivo}"
|
105
|
+
next
|
75
106
|
end
|
76
107
|
|
77
|
-
#
|
108
|
+
# Geração de feature
|
78
109
|
feature_path, feature_content = Generator.gerar_feature(historia)
|
79
110
|
Backup.salvar_versao_antiga(feature_path)
|
80
111
|
features += 1 if Generator.salvar_feature(feature_path, feature_content)
|
81
112
|
|
82
|
-
#
|
113
|
+
# Geração de steps
|
83
114
|
if StepsGenerator.gerar_passos(feature_path)
|
84
115
|
steps += 1
|
85
116
|
else
|
86
117
|
skipped_steps << feature_path
|
87
118
|
end
|
88
119
|
|
89
|
-
#
|
120
|
+
# Exportação de PDF (apenas novos)
|
90
121
|
FileUtils.mkdir_p('reports')
|
91
|
-
|
92
|
-
generated_pdfs.concat(
|
93
|
-
skipped_pdfs.concat(
|
122
|
+
result = PDFExporter.exportar_todos(only_new: true)
|
123
|
+
generated_pdfs.concat(result[:generated])
|
124
|
+
skipped_pdfs.concat(result[:skipped])
|
94
125
|
end
|
95
126
|
|
96
|
-
#
|
127
|
+
# Exibe relatório final
|
97
128
|
puts "\n✅ Processamento concluído"
|
98
129
|
puts "- Total de histórias: #{total}"
|
99
130
|
puts "- Features geradas: #{features}"
|
@@ -101,7 +132,7 @@ module Bddgenx
|
|
101
132
|
puts "- Steps ignorados: #{skipped_steps.size}"
|
102
133
|
puts "- PDFs gerados: #{generated_pdfs.size}"
|
103
134
|
puts "- PDFs já existentes: #{skipped_pdfs.size}"
|
104
|
-
puts "-
|
135
|
+
puts "- Histórias ignoradas: #{ignored}"
|
105
136
|
end
|
106
137
|
end
|
107
138
|
end
|
@@ -1,108 +1,137 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
require 'strscan' # para usar ::StringScanner
|
3
|
-
require_relative 'utils/tipo_param'
|
4
|
-
|
5
1
|
# lib/bddgenx/steps_generator.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# Este arquivo define a classe StepsGenerator, responsável por gerar
|
5
|
+
# definições de passos do Cucumber a partir de arquivos .feature.
|
6
|
+
# Suporta palavras-chave Gherkin em Português e Inglês e parametriza
|
7
|
+
# strings e números conforme necessário.
|
8
|
+
|
9
|
+
require 'fileutils'
|
10
|
+
require 'strscan' # Para uso de StringScanner
|
6
11
|
|
7
12
|
module Bddgenx
|
13
|
+
# Gera arquivos de definições de passos Ruby para Cucumber
|
14
|
+
# com base em arquivos .feature.
|
8
15
|
class StepsGenerator
|
16
|
+
# Palavras-chave Gherkin em Português usadas em arquivos de feature
|
17
|
+
# @return [Array<String>]
|
9
18
|
GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
|
19
|
+
|
20
|
+
# Palavras-chave Gherkin em Inglês usadas em arquivos de feature
|
21
|
+
# @return [Array<String>]
|
10
22
|
GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
|
11
|
-
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
12
23
|
|
13
|
-
#
|
24
|
+
# Conjunto de todas as palavras-chave suportadas (PT + EN)
|
25
|
+
# @return [Array<String>]
|
26
|
+
ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
|
27
|
+
|
28
|
+
# Converte uma string para camelCase, útil para nomes de argumentos
|
29
|
+
#
|
30
|
+
# @param [String] str Texto de entrada a ser convertido
|
31
|
+
# @return [String] Versão em camelCase do texto
|
14
32
|
def self.camelize(str)
|
15
|
-
|
16
|
-
|
33
|
+
partes = str.strip.split(/[^a-zA-Z0-9]+/)
|
34
|
+
partes.map.with_index { |palavra, i| i.zero? ? palavra.downcase : palavra.capitalize }.join
|
17
35
|
end
|
18
36
|
|
19
|
-
# Gera
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# Respeita idioma de entrada (pt/en) para keywords geradas
|
37
|
+
# Gera definições de passos a partir de um arquivo .feature
|
38
|
+
#
|
39
|
+
# @param [String] feature_path Caminho para o arquivo .feature
|
40
|
+
# @raise [ArgumentError] Se feature_path não for String
|
41
|
+
# @return [Boolean] Retorna true se passos foram gerados, false se não houver passos
|
25
42
|
def self.gerar_passos(feature_path)
|
26
|
-
|
43
|
+
# Valida tipo de entrada
|
44
|
+
unless feature_path.is_a?(String)
|
45
|
+
raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}"
|
46
|
+
end
|
47
|
+
|
48
|
+
linhas = File.readlines(feature_path)
|
27
49
|
|
28
|
-
lines = File.readlines(feature_path)
|
29
50
|
# Detecta idioma no cabeçalho: "# language: pt" ou "# language: en"
|
30
|
-
lang = if (m =
|
51
|
+
lang = if (m = linhas.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
|
31
52
|
m[/^#\s*language:\s*(\w+)/i, 1].downcase
|
32
53
|
else
|
33
54
|
'pt'
|
34
55
|
end
|
35
56
|
|
36
|
-
|
37
|
-
|
57
|
+
# Mapas de tradução entre PT e EN
|
58
|
+
pt_para_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
|
59
|
+
en_para_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
|
38
60
|
|
39
|
-
# Seleciona
|
40
|
-
|
41
|
-
ALL_KEYS.any? { |
|
61
|
+
# Seleciona linhas que começam com palavras-chave Gherkin
|
62
|
+
linhas_passos = linhas.map(&:strip).select do |linha|
|
63
|
+
ALL_KEYS.any? { |chave| linha.start_with?(chave + ' ') }
|
42
64
|
end
|
43
|
-
return false if step_lines.empty?
|
44
65
|
|
45
|
-
|
46
|
-
|
47
|
-
file = File.join(dir, "#{File.basename(feature_path, '.feature')}_steps.rb")
|
66
|
+
# Se não encontrar passos, retorna false
|
67
|
+
return false if linhas_passos.empty?
|
48
68
|
|
49
|
-
|
69
|
+
# Cria diretório e arquivo de saída
|
70
|
+
dir_saida = File.join(File.dirname(feature_path), 'steps')
|
71
|
+
FileUtils.mkdir_p(dir_saida)
|
72
|
+
arquivo_saida = File.join(dir_saida, "#{File.basename(feature_path, '.feature')}_steps.rb")
|
50
73
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
74
|
+
# Cabeçalho do arquivo gerado
|
75
|
+
conteudo = +"# encoding: utf-8\n"
|
76
|
+
conteudo << "# Definições de passos geradas automaticamente para #{File.basename(feature_path)}\n\n"
|
77
|
+
|
78
|
+
linhas_passos.each do |linha|
|
79
|
+
palavra_original, restante = linha.split(' ', 2)
|
80
|
+
|
81
|
+
# Define palavra-chave no idioma de saída
|
82
|
+
chave = case lang
|
83
|
+
when 'en' then pt_para_en[palavra_original] || palavra_original
|
84
|
+
else en_para_pt[palavra_original] || palavra_original
|
85
|
+
end
|
60
86
|
|
61
|
-
|
62
|
-
|
63
|
-
|
87
|
+
texto_bruto = restante.dup
|
88
|
+
scanner = ::StringScanner.new(restante)
|
89
|
+
padrao = ''
|
90
|
+
tokens = []
|
64
91
|
|
65
92
|
until scanner.eos?
|
66
93
|
if scanner.check(/"<([^>]+)>"/)
|
67
94
|
scanner.scan(/"<([^>]+)>"/)
|
68
95
|
tokens << scanner[1]
|
69
|
-
|
96
|
+
padrao << '{string}'
|
70
97
|
elsif scanner.check(/<([^>]+)>/)
|
71
98
|
scanner.scan(/<([^>]+)>/)
|
72
99
|
tokens << scanner[1]
|
73
|
-
|
100
|
+
padrao << '{int}'
|
74
101
|
elsif scanner.check(/"([^"<>]+)"/)
|
75
102
|
scanner.scan(/"([^"<>]+)"/)
|
76
103
|
tokens << scanner[1]
|
77
|
-
|
104
|
+
padrao << '{string}'
|
78
105
|
elsif scanner.check(/\d+(?:\.\d+)?/)
|
79
|
-
|
80
|
-
tokens <<
|
81
|
-
|
106
|
+
numero = scanner.scan(/\d+(?:\.\d+)?/)
|
107
|
+
tokens << numero
|
108
|
+
padrao << '{int}'
|
82
109
|
else
|
83
|
-
|
110
|
+
padrao << scanner.getch
|
84
111
|
end
|
85
112
|
end
|
86
113
|
|
87
|
-
# Escapa aspas no padrão
|
88
|
-
|
89
|
-
|
114
|
+
# Escapa aspas no padrão
|
115
|
+
padrao_seguro = padrao.gsub('"', '\\"')
|
116
|
+
assinatura = "#{chave}(\"#{padrao_seguro}\")"
|
90
117
|
|
118
|
+
# Adiciona parâmetros se existirem tokens
|
91
119
|
if tokens.any?
|
92
|
-
|
93
|
-
|
120
|
+
argumentos = tokens.each_index.map { |i| "arg#{i+1}" }.join(', ')
|
121
|
+
assinatura << " do |#{argumentos}|"
|
94
122
|
else
|
95
|
-
|
123
|
+
assinatura << ' do'
|
96
124
|
end
|
97
125
|
|
98
|
-
|
99
|
-
|
100
|
-
|
126
|
+
conteudo << "#{assinatura}\n"
|
127
|
+
conteudo << " pending 'Implementar passo: #{texto_bruto}'\n"
|
128
|
+
conteudo << "end\n\n"
|
101
129
|
end
|
102
130
|
|
103
|
-
|
104
|
-
|
131
|
+
# Escreve arquivo de saída
|
132
|
+
File.write(arquivo_saida, conteudo)
|
133
|
+
puts "✅ Steps gerados: #{arquivo_saida}"
|
105
134
|
true
|
106
135
|
end
|
107
136
|
end
|
108
|
-
end
|
137
|
+
end
|
data/lib/bddgenx/utils/backup.rb
CHANGED
@@ -1,16 +1,31 @@
|
|
1
|
+
# lib/bddgenx/backup.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# Este arquivo define a classe Backup, responsável por criar cópias de segurança
|
5
|
+
# de arquivos .feature antes de serem sobrescritos.
|
6
|
+
# As cópias são salvas em 'reports/backup' com timestamp no nome.
|
7
|
+
|
1
8
|
require 'fileutils'
|
2
9
|
require 'time'
|
3
10
|
|
4
11
|
module Bddgenx
|
12
|
+
# Gerencia a criação de backups de arquivos .feature
|
5
13
|
class Backup
|
14
|
+
# Salva uma versão antiga de um arquivo .feature em reports/backup,
|
15
|
+
# adicionando um timestamp ao nome do arquivo.
|
16
|
+
#
|
17
|
+
# @param caminho [String] Caminho completo para o arquivo .feature original
|
18
|
+
# @return [void]
|
19
|
+
# @note Se o arquivo não existir, não faz nada
|
6
20
|
def self.salvar_versao_antiga(caminho)
|
7
21
|
return unless File.exist?(caminho)
|
8
22
|
|
9
|
-
pasta
|
23
|
+
pasta = 'reports/backup'
|
10
24
|
FileUtils.mkdir_p(pasta)
|
11
|
-
|
12
|
-
|
13
|
-
|
25
|
+
|
26
|
+
base = File.basename(caminho, '.feature')
|
27
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
28
|
+
destino = "reports/backup/#{base}_#{timestamp}.feature"
|
14
29
|
|
15
30
|
FileUtils.cp(caminho, destino)
|
16
31
|
puts "📦 Backup criado: #{destino}"
|
@@ -1,28 +1,46 @@
|
|
1
1
|
# lib/bddgenx/font_loader.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# Este arquivo define a classe FontLoader, responsável por localizar e carregar
|
5
|
+
# famílias de fontes TrueType para uso com Prawn em geração de PDFs.
|
6
|
+
# Busca os arquivos de fonte no diretório assets/fonts dentro da gem.
|
7
|
+
|
2
8
|
require 'prawn'
|
3
|
-
require 'rubygems' # para Gem.loaded_specs
|
9
|
+
require 'rubygems' # para Gem.loaded_specs se necessário
|
4
10
|
|
5
11
|
module Bddgenx
|
12
|
+
# Gerencia o carregamento de fontes TTF para os documentos PDF.
|
6
13
|
class FontLoader
|
7
|
-
# Retorna o
|
14
|
+
# Retorna o caminho absoluto para a pasta assets/fonts dentro da gem
|
15
|
+
#
|
16
|
+
# @return [String] Caminho completo para o diretório de fontes
|
8
17
|
def self.fonts_path
|
9
18
|
File.expand_path('../../assets/fonts', __dir__)
|
10
19
|
end
|
11
20
|
|
12
|
-
#
|
21
|
+
# Cria o hash de famílias de fontes para registro no Prawn
|
22
|
+
#
|
23
|
+
# Verifica se os arquivos de fonte DejaVuSansMono incluem normal, bold,
|
24
|
+
# italic e bold_italic e têm tamanho mínimo aceitável.
|
25
|
+
# Se estiverem ausentes ou corrompidas, retorna hash vazio para usar fallback.
|
26
|
+
#
|
27
|
+
# @return [Hash{String => Hash<Symbol, String>}]
|
28
|
+
# - Chave: nome da família ('DejaVuSansMono')
|
29
|
+
# - Valor: mapa de estilos (:normal, :bold, :italic, :bold_italic) para os caminhos dos arquivos
|
13
30
|
def self.families
|
14
31
|
base = fonts_path
|
15
32
|
return {} unless Dir.exist?(base)
|
16
33
|
|
17
|
-
|
34
|
+
arquivos = {
|
18
35
|
normal: File.join(base, 'DejaVuSansMono.ttf'),
|
19
36
|
bold: File.join(base, 'DejaVuSansMono-Bold.ttf'),
|
20
37
|
italic: File.join(base, 'DejaVuSansMono-Oblique.ttf'),
|
21
38
|
bold_italic: File.join(base, 'DejaVuSansMono-BoldOblique.ttf')
|
22
39
|
}
|
23
40
|
|
24
|
-
|
25
|
-
|
41
|
+
# Verifica existência e tamanho mínimo de cada arquivo
|
42
|
+
if arquivos.values.all? { |path| File.file?(path) && File.size(path) > 12 }
|
43
|
+
{ 'DejaVuSansMono' => arquivos }
|
26
44
|
else
|
27
45
|
warn "⚠️ Fontes DejaVuSansMono ausentes ou corrompidas em #{base}. Usando fallback Courier."
|
28
46
|
{}
|
data/lib/bddgenx/utils/parser.rb
CHANGED
@@ -1,25 +1,56 @@
|
|
1
|
+
# lib/bddgenx/parser.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# Este arquivo define a classe Parser, responsável por ler e interpretar
|
5
|
+
# arquivos de história (.txt), extraindo cabeçalho e blocos de passos e exemplos.
|
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
|
+
|
1
9
|
module Bddgenx
|
2
|
-
# Tipos de blocos
|
10
|
+
# Tipos de blocos reconhecidos na história (.txt), incluindo variações em Português
|
11
|
+
# e Inglês para blocos de exemplo.
|
12
|
+
# @return [Array<String>]
|
3
13
|
TIPOS_BLOCOS = %w[
|
4
14
|
CONTEXT SUCCESS FAILURE ERROR EXCEPTION
|
5
15
|
VALIDATION PERMISSION EDGE_CASE PERFORMANCE
|
6
16
|
EXEMPLO EXEMPLOS RULE
|
7
17
|
].freeze
|
8
18
|
|
19
|
+
# Parser de arquivos de história, converte .txt em estrutura de hash
|
20
|
+
# com elementos :como, :quero, :para, :idioma e lista de :grupos.
|
9
21
|
class Parser
|
10
|
-
# Lê arquivo de história
|
22
|
+
# Lê e analisa um arquivo de história, retornando um Hash com a estrutura:
|
23
|
+
# {
|
24
|
+
# como: String ou nil,
|
25
|
+
# quero: String ou nil,
|
26
|
+
# para: String ou nil,
|
27
|
+
# idioma: 'pt' ou 'en',
|
28
|
+
# grupos: [
|
29
|
+
# {
|
30
|
+
# tipo: String (tipo de bloco),
|
31
|
+
# tag: String ou nil (tag opcional após o bloco),
|
32
|
+
# passos: Array<String> (linhas de passo),
|
33
|
+
# exemplos: Array<String> (linhas de exemplo)
|
34
|
+
# },
|
35
|
+
# ...
|
36
|
+
# ]
|
37
|
+
# }
|
38
|
+
#
|
39
|
+
# @param caminho_arquivo [String] Caminho para o arquivo .txt de história
|
40
|
+
# @raise [Errno::ENOENT] Se o arquivo não for encontrado
|
41
|
+
# @return [Hash] Estrutura da história pronta para geração de feature
|
11
42
|
def self.ler_historia(caminho_arquivo)
|
12
|
-
#
|
43
|
+
# Carrega linhas do arquivo, preservando linhas vazias e encoding UTF-8
|
13
44
|
linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:rstrip)
|
14
45
|
|
15
|
-
# Detecta idioma:
|
46
|
+
# Detecta idioma no topo do arquivo: suporta '# lang: <codigo>' ou '# language: <codigo>'
|
16
47
|
idioma = 'pt'
|
17
48
|
if linhas.first =~ /^#\s*lang(?:uage)?\s*:\s*(\w+)/i
|
18
49
|
idioma = Regexp.last_match(1).downcase
|
19
50
|
linhas.shift
|
20
51
|
end
|
21
52
|
|
22
|
-
# Extrai
|
53
|
+
# Extrai cabeçalho: linhas que começam com Como/Quero/Para (variações PT/EN)
|
23
54
|
como, quero, para = nil, nil, nil
|
24
55
|
linhas.reject! do |l|
|
25
56
|
if l =~ /^\s*(?:como|eu como|as a)/i && como.nil?
|
@@ -36,19 +67,27 @@ module Bddgenx
|
|
36
67
|
end
|
37
68
|
end
|
38
69
|
|
39
|
-
|
70
|
+
# Inicializa estrutura da história
|
71
|
+
historia = {
|
72
|
+
como: como,
|
73
|
+
quero: quero,
|
74
|
+
para: para,
|
75
|
+
idioma: idioma,
|
76
|
+
grupos: []
|
77
|
+
}
|
40
78
|
exemplos_mode = false
|
41
79
|
|
80
|
+
# Processa cada linha restante para blocos e exemplos
|
42
81
|
linhas.each do |linha|
|
43
|
-
# Início de bloco de exemplos: [EXEMPLO]
|
82
|
+
# Início de bloco de exemplos: [EXEMPLO], [EXEMPLOS] ou [EXAMPLES] com tag opcional
|
44
83
|
if linha =~ /^\[(?:EXEMPLO|EXEMPLOS|EXAMPLES)\](?:@(\w+))?$/i
|
45
84
|
exemplos_mode = true
|
46
|
-
#
|
85
|
+
# Cria array de exemplos no último grupo, se ainda não existir
|
47
86
|
historia[:grupos].last[:exemplos] = []
|
48
87
|
next
|
49
88
|
end
|
50
89
|
|
51
|
-
# Início de bloco
|
90
|
+
# Início de bloco com tipo definido em TIPOS_BLOCOS e tag opcional
|
52
91
|
if linha =~ /^\[(#{TIPOS_BLOCOS.join('|')})\](?:@(\w+))?$/i
|
53
92
|
exemplos_mode = false
|
54
93
|
tipo = Regexp.last_match(1).upcase
|
@@ -57,7 +96,7 @@ module Bddgenx
|
|
57
96
|
next
|
58
97
|
end
|
59
98
|
|
60
|
-
# Atribui
|
99
|
+
# Atribui linha ao último bloco, como passo ou exemplo conforme modo
|
61
100
|
next if historia[:grupos].empty?
|
62
101
|
bloco = historia[:grupos].last
|
63
102
|
if exemplos_mode
|