spec_tracker 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +48 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +79 -0
- data/LICENSE +24 -0
- data/README.md +84 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/spec_tracker +5 -0
- data/lib/spec_tracker.rb +50 -0
- data/lib/spec_tracker/cli.rb +24 -0
- data/lib/spec_tracker/config/configuration.rb +23 -0
- data/lib/spec_tracker/config/en.yml +8 -0
- data/lib/spec_tracker/config/fr.yml +8 -0
- data/lib/spec_tracker/report_task.rb +26 -0
- data/lib/spec_tracker/reporter/base_reporter.rb +24 -0
- data/lib/spec_tracker/reporter/base_ui_mapper.rb +25 -0
- data/lib/spec_tracker/reporter/report.rb +17 -0
- data/lib/spec_tracker/reporter/report_datum.rb +13 -0
- data/lib/spec_tracker/reporter/report_mapper.rb +19 -0
- data/lib/spec_tracker/reporter/status_ui.rb +8 -0
- data/lib/spec_tracker/reporter/terminal/reporter.rb +28 -0
- data/lib/spec_tracker/reporter/terminal/ui_mapper.rb +13 -0
- data/lib/spec_tracker/spec_parser/base_parser.rb +36 -0
- data/lib/spec_tracker/spec_parser/csv_parser.rb +24 -0
- data/lib/spec_tracker/spec_parser/gherkin_parser.rb +27 -0
- data/lib/spec_tracker/spec_parser/scenario.rb +12 -0
- data/lib/spec_tracker/spec_parser/specification.rb +12 -0
- data/lib/spec_tracker/test_report_parser/base_mapper.rb +52 -0
- data/lib/spec_tracker/test_report_parser/base_parser.rb +43 -0
- data/lib/spec_tracker/test_report_parser/j_unit/mapper.rb +25 -0
- data/lib/spec_tracker/test_report_parser/j_unit/parser.rb +31 -0
- data/lib/spec_tracker/test_report_parser/test_result.rb +12 -0
- data/lib/spec_tracker/test_status.rb +46 -0
- data/lib/spec_tracker/version.rb +3 -0
- data/spec_tracker.gemspec +39 -0
- data/specifications/Spec Fonctionnelles SpecTracker - Configuration.csv +11 -0
- data/specifications/Spec Fonctionnelles SpecTracker - Parse specifications.csv +20 -0
- data/specifications/configuration.feature +11 -0
- data/specifications/parse_csv_specifications.feature +11 -0
- data/specifications/parse_gherkin_specifications.feature +11 -0
- metadata +249 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
class CLI < Thor
|
3
|
+
desc 'report', 'generate a report for a given configuration (default or custom)'
|
4
|
+
method_option :spec_path, type: :string, desc: "Path to the specifications. Expects a relative path to a file or a directory. Default: specifications"
|
5
|
+
method_option :test_report_path, type: :string, desc: "Path to the test reports. Expects a relative path to a file or a directory. Default: test/reports"
|
6
|
+
method_option :scenario_id_regex, type: :string, desc: "Regexp to find scenario ID in test reports. Default : \\[([a-zA-Z\\-]+)\\]"
|
7
|
+
method_option :spec_type, enum: %w(csv gherkin), desc: "Type of specification files. Default : csv"
|
8
|
+
method_option :scenario_id_header, type: :string, desc: "Name of the scenario ID column when spec files are csv. Default: #Scenario"
|
9
|
+
method_option :scenario_name_header, type: :string, desc: "Name of the scenario name column when spec files are csv. Default: Name/Rule"
|
10
|
+
method_option :locale, enum: %w(fr en), desc: "Locale for text output. Default: fr"
|
11
|
+
def report
|
12
|
+
SpecTracker.configure do |configuration|
|
13
|
+
configuration.spec_path = options[:spec_path] if options[:spec_path]
|
14
|
+
configuration.test_report_path = options[:test_report_path] if options[:test_report_path]
|
15
|
+
configuration.scenario_id_regex = Regexp.new(options[:scenario_id_regex]) if options[:scenario_id_regex]
|
16
|
+
configuration.spec_type = options[:spec_type] if options[:spec_type]
|
17
|
+
configuration.scenario_id_header = options[:scenario_id_header] if options[:scenario_id_header]
|
18
|
+
configuration.scenario_name_header = options[:scenario_name_header] if options[:scenario_name_header]
|
19
|
+
configuration.locale = options[:locale] if options[:locale]
|
20
|
+
end
|
21
|
+
SpecTracker::ReportTask.new.execute
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :scenario_id_header, :scenario_name_header, :locale,
|
4
|
+
:test_report_path, :spec_path, :scenario_id_regex, :spec_type
|
5
|
+
|
6
|
+
attr_reader :wording, :local_path
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@scenario_id_header = 'Scenario ID'
|
10
|
+
@scenario_name_header = 'Name/Rule'
|
11
|
+
@spec_path = 'specifications'
|
12
|
+
@test_report_path = 'test/reports'
|
13
|
+
@scenario_id_regex = /\[([a-zA-Z\-]+)\]/
|
14
|
+
@spec_type = 'csv'
|
15
|
+
@available_types = %w(csv gherkin)
|
16
|
+
@locale = 'fr'
|
17
|
+
@available_locales = %w(fr en)
|
18
|
+
wording_path = Pathname.new(__FILE__).join("../#{locale}.yml")
|
19
|
+
@wording = YAML.load_file(wording_path)[:"#{locale}"]
|
20
|
+
@local_path = Pathname.new(Dir.pwd)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
:fr:
|
2
|
+
:missing_specs: Le fichier de spécification n'a pas été fourni
|
3
|
+
:bad_format: Le fichier de specs n'est pas un csv
|
4
|
+
:too_many_arguments: Trop d'arguments ont été fournis
|
5
|
+
:scenario_id: Identifiant du scénario
|
6
|
+
:scenario_name: Nom du scénario
|
7
|
+
:test_result: Statut du test
|
8
|
+
:progression: Progression
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
class ReportTask
|
3
|
+
attr_reader :spec_parser, :test_report_parser, :report_mapper, :reporter
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
spec_type = SpecTracker.configuration.spec_type
|
7
|
+
@spec_parser = (spec_type == %q{gherkin}) ? SpecParser::GherkinParser.new : SpecParser::CSVParser.new
|
8
|
+
@test_report_parser = TestReportParser::JUnit::Parser.new
|
9
|
+
@report_mapper = Reporter::ReportMapper.new
|
10
|
+
@reporter = Reporter::Terminal::Reporter.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
specifications = spec_parser.parse(SpecTracker.configuration.spec_path)
|
15
|
+
test_results = test_report_parser.parse(SpecTracker.configuration.test_report_path)
|
16
|
+
specifications.map do |specification|
|
17
|
+
report = report_mapper.map(
|
18
|
+
topic: specification.topic,
|
19
|
+
scenarios: specification.scenarios,
|
20
|
+
test_results: test_results
|
21
|
+
)
|
22
|
+
reporter.print(report)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
class BaseReporter
|
4
|
+
def initialize
|
5
|
+
# Should initialize UIMapper
|
6
|
+
raise Error.new('override me!')
|
7
|
+
end
|
8
|
+
|
9
|
+
def print(_)
|
10
|
+
raise Error.new('override me!')
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def view_models(report_data)
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :ui_mapper
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
class BaseUIMapper
|
4
|
+
include StatusUI
|
5
|
+
|
6
|
+
def map(_)
|
7
|
+
raise Error.new('override me!')
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def status_to_ui(status)
|
13
|
+
if status.success?
|
14
|
+
SUCCESS_EMOJI
|
15
|
+
elsif status.failure?
|
16
|
+
FAILURE_EMOJI
|
17
|
+
elsif status.skipped?
|
18
|
+
SKIPPED_EMOJI
|
19
|
+
elsif status.missing?
|
20
|
+
MISSING_EMOJI
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
class Report
|
4
|
+
attr_reader :data, :topic
|
5
|
+
|
6
|
+
def initialize(data:, topic:)
|
7
|
+
@data = data
|
8
|
+
@topic = topic
|
9
|
+
end
|
10
|
+
|
11
|
+
def progression
|
12
|
+
success_count = data.select {|report_datum| report_datum.status.success?}.size
|
13
|
+
(success_count.to_f / data.size.to_f * 100).round(1)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
class ReportDatum
|
4
|
+
attr_reader :scenario_id, :scenario_name, :status
|
5
|
+
|
6
|
+
def initialize(scenario_id:, scenario_name:, status:)
|
7
|
+
@scenario_id = scenario_id
|
8
|
+
@scenario_name = scenario_name
|
9
|
+
@status = status
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
class ReportMapper
|
4
|
+
def map(scenarios:, test_results:, topic:)
|
5
|
+
report_data = scenarios.map do |scenario|
|
6
|
+
test_result = test_results.detect {|test_result| test_result.scenario_id == scenario.id}
|
7
|
+
if test_result
|
8
|
+
test_status = test_result.status
|
9
|
+
else
|
10
|
+
test_status = TestStatus.new
|
11
|
+
test_status.missing!
|
12
|
+
end
|
13
|
+
ReportDatum.new(scenario_id: scenario.id, scenario_name: scenario.name, status: test_status)
|
14
|
+
end
|
15
|
+
Report.new(data: report_data, topic: topic)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module StatusUI
|
3
|
+
SUCCESS_EMOJI = Emoji.find_by_alias("white_check_mark").raw.freeze
|
4
|
+
FAILURE_EMOJI = Emoji.find_by_alias("x").raw.freeze
|
5
|
+
SKIPPED_EMOJI = Emoji.find_by_alias("fast_forward").raw.freeze
|
6
|
+
MISSING_EMOJI = Emoji.find_by_alias("no_mouth").raw.freeze
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
module Terminal
|
4
|
+
class Reporter < BaseReporter
|
5
|
+
def initialize
|
6
|
+
@ui_mapper = UIMapper.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def print(report)
|
10
|
+
terminal_table = ::Terminal::Table.new do |t|
|
11
|
+
t.title = report.topic
|
12
|
+
t.headings = [
|
13
|
+
SpecTracker.configuration.wording[:scenario_id],
|
14
|
+
SpecTracker.configuration.wording[:scenario_name],
|
15
|
+
SpecTracker.configuration.wording[:test_result]
|
16
|
+
]
|
17
|
+
t.rows = ui_mapper.map(report)
|
18
|
+
t.add_separator
|
19
|
+
t.add_row [SpecTracker.configuration.wording[:progression], nil, "#{report.progression}%"]
|
20
|
+
end
|
21
|
+
terminal_table.align_column(2, :center)
|
22
|
+
terminal_table.align_column(0, :center)
|
23
|
+
puts terminal_table
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module Reporter
|
3
|
+
module Terminal
|
4
|
+
class UIMapper < BaseUIMapper
|
5
|
+
def map(report)
|
6
|
+
report.data.map do |report_datum|
|
7
|
+
[report_datum.scenario_id, report_datum.scenario_name, status_to_ui(report_datum.status)]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module SpecParser
|
3
|
+
class BaseParser
|
4
|
+
def parse(path)
|
5
|
+
specifications = []
|
6
|
+
if spec_path(path).directory?
|
7
|
+
spec_path(path).each_entry do |entry|
|
8
|
+
next unless entry.extname == spec_file_extension
|
9
|
+
scenarios = parse_single(spec_path(path).join(entry.basename))
|
10
|
+
specifications << Specification.new(topic: entry.basename, scenarios: scenarios)
|
11
|
+
end
|
12
|
+
else
|
13
|
+
scenarios = parse_single(spec_path(path))
|
14
|
+
specifications << Specification.new(topic: spec_path(path).basename, scenarios: scenarios)
|
15
|
+
end
|
16
|
+
specifications
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def spec_file_extension
|
22
|
+
raise Error.new('override me!')
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_single(spec_file)
|
26
|
+
raise Error.new('override me!')
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def spec_path(path)
|
32
|
+
SpecTracker.configuration.local_path.join(path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module SpecParser
|
3
|
+
class CSVParser < BaseParser
|
4
|
+
private
|
5
|
+
|
6
|
+
def spec_file_extension
|
7
|
+
'.csv'
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_single(csv_file)
|
11
|
+
scenarios = []
|
12
|
+
scenario_id_header = SpecTracker.configuration.scenario_id_header
|
13
|
+
scenario_name_header = SpecTracker.configuration.scenario_name_header
|
14
|
+
CSV.foreach(csv_file, headers: true, skip_blanks: true) do |row|
|
15
|
+
next if row[scenario_id_header].nil?
|
16
|
+
scenario_id = row[scenario_id_header]
|
17
|
+
scenario_name = row[scenario_name_header]
|
18
|
+
scenarios << Scenario.new(id: scenario_id, name: scenario_name)
|
19
|
+
end
|
20
|
+
scenarios
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module SpecParser
|
3
|
+
class GherkinParser < BaseParser
|
4
|
+
def initialize
|
5
|
+
@language_parser = Gherkin::Parser.new
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def spec_file_extension
|
11
|
+
'.feature'
|
12
|
+
end
|
13
|
+
|
14
|
+
# FIXME: handle large files
|
15
|
+
def parse_single(spec_file)
|
16
|
+
file_content = File.read(spec_file)
|
17
|
+
parse_result = @language_parser.parse(file_content)
|
18
|
+
parse_result[:feature][:children].select { |child| child[:type].downcase == :scenario }.map do |scenario|
|
19
|
+
scenario_id = scenario[:name].slice(SpecTracker.configuration.scenario_id_regex, 1)
|
20
|
+
next if scenario_id.nil?
|
21
|
+
scenario_name = scenario[:name].gsub(SpecTracker.configuration.scenario_id_regex, '').strip
|
22
|
+
Scenario.new(id: scenario_id, name: scenario_name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module TestReportParser
|
3
|
+
class BaseMapper
|
4
|
+
def map(test_case)
|
5
|
+
name = get_scenario_name(test_case)
|
6
|
+
scenario_id = get_scenario_id(name)
|
7
|
+
status = get_scenario_status(test_case)
|
8
|
+
TestResult.new(scenario_id: scenario_id, status: status)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def get_scenario_name(_)
|
14
|
+
raise Error.new('override me!')
|
15
|
+
end
|
16
|
+
|
17
|
+
def failed?(_)
|
18
|
+
raise Error.new('override me!')
|
19
|
+
end
|
20
|
+
|
21
|
+
def success?(_)
|
22
|
+
raise Error.new('override me!')
|
23
|
+
end
|
24
|
+
|
25
|
+
def skipped?(_)
|
26
|
+
raise Error.new('override me!')
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def get_scenario_id(scenario_name)
|
32
|
+
scenario_name.slice(scenario_id_regex, 1)
|
33
|
+
end
|
34
|
+
|
35
|
+
def scenario_id_regex
|
36
|
+
SpecTracker.configuration.scenario_id_regex
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_scenario_status(testcase)
|
40
|
+
test_status = TestStatus.new
|
41
|
+
if success?(testcase)
|
42
|
+
test_status.success!
|
43
|
+
elsif skipped?(testcase)
|
44
|
+
test_status.skipped!
|
45
|
+
else
|
46
|
+
test_status.failure!
|
47
|
+
end
|
48
|
+
test_status
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SpecTracker
|
2
|
+
module TestReportParser
|
3
|
+
class BaseParser
|
4
|
+
def parse(path)
|
5
|
+
test_results = []
|
6
|
+
if report_path(path).directory?
|
7
|
+
report_path(path).each_entry do |entry|
|
8
|
+
results = parse_single(report_path(path).join(entry.basename))
|
9
|
+
test_results.concat(results) unless results.empty?
|
10
|
+
end
|
11
|
+
else
|
12
|
+
results = parse_single(report_path(path))
|
13
|
+
test_results.concat(results) unless results.empty?
|
14
|
+
end
|
15
|
+
test_results
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def parse_single(file)
|
21
|
+
raise Error.new('override me!')
|
22
|
+
end
|
23
|
+
|
24
|
+
def file_extension
|
25
|
+
raise Error.new('override me!')
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_cases(_)
|
29
|
+
raise Error.new('override me!')
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_extension?(entry)
|
33
|
+
entry.extname == file_extension
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def report_path(path)
|
39
|
+
SpecTracker.configuration.local_path.join(path)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|