cuporter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2010 ThoughtWorks, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,72 @@
1
+ h1. Cuporter
2
+
3
+ Scrapes your feature files and shows scenarios per tag, with their context. Formats Pretty Text, HTML (not styled yet) and CSV.
4
+
5
+ Consider this a stop-gap until we get this functionality in a proper cucumber formatter.
6
+
7
+ ---------
8
+ h3. Example Output
9
+
10
+ <pre>
11
+ @failing
12
+ Feature: Abominable Aardvark
13
+ Scenario: An Aardvark eats ants
14
+ Scenario: Zee Zebra eats zee aardvark
15
+ Feature: Wired
16
+ Scenario: Everybody's Wired
17
+ Scenario Outline: Why is everybody so wired?
18
+ Examples: loosely wired
19
+ Examples: tightly wired
20
+ @ignore
21
+ Feature: Wired
22
+ Scenario Outline: Why is everybody so wired?
23
+ Examples: tightly wired
24
+ @wip
25
+ Feature: HTML formatter
26
+ Scenario: Everything in fixtures/self_test
27
+ Feature: not everyone is involved
28
+ Scenario: Failure is not an option, people
29
+ Feature: sample
30
+ Scenario: And yet another Example
31
+ Feature: search examples
32
+ Scenario: Generate PDF with pdf formatter
33
+
34
+ </pre>
35
+
36
+ ---------
37
+
38
+ h3. Command Lines
39
+
40
+ h4. help
41
+
42
+ <pre>
43
+ $ ./bin/cuporter.rb -h
44
+
45
+ Usage: cuporter.rb [options]
46
+
47
+ -i, --in DIR directory of *.feature files
48
+ Default: features/**/*.feature
49
+
50
+ --input-file FILE full file name with extension: 'path/to/file.feature'
51
+
52
+ -o, --out FILE Output file path
53
+
54
+ -f, --format [pretty|html|csv] Output format
55
+ Default: pretty text
56
+ </pre>
57
+
58
+ h4. run script directly
59
+
60
+ <pre>
61
+ $ ./bin/cuporter.rb -i fixtures/self_text # pretty-print demo report to stdout
62
+
63
+ $ ./bin/cuporter.rb -f html -o feature_tag_report.html # default input features/**/*.feature to named output file
64
+ </pre>
65
+
66
+ h4. run via rake
67
+
68
+ <pre>
69
+ $ rake cuporter:run["-i fixtures/self_text"]
70
+
71
+ $ rake cuporter:run["-f html -o feature_tag_report.html"]
72
+ </pre>
data/Rakefile ADDED
@@ -0,0 +1,86 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rspec/core/rake_task'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+
7
+ task :default do
8
+ Rake.application.tasks_in_scope(["cuporter:test"]).each do |t|
9
+ t.invoke
10
+ end
11
+ end
12
+
13
+ namespace :cuporter do
14
+
15
+ # $ rake cuporter:run["-f html cucumber_tag_report.html"]
16
+ desc "run cuporter command line with options"
17
+ task :run, [:options] do |t, args|
18
+ sh "ruby ./bin/cuporter #{args.options}"
19
+ end
20
+
21
+ task :readme do
22
+ require 'redcloth'
23
+ puts RedCloth.new(File.read("README.textile")).to_html
24
+ end
25
+
26
+ namespace :test do
27
+ desc "unit specs"
28
+ RSpec::Core::RakeTask.new(:unit) do |t|
29
+ t.pattern = "spec/cuporter/*_spec.rb"
30
+ t.spec_opts = ["--color" , "--format" , "doc" ]
31
+ end
32
+
33
+ desc "functional specs against feature fixtures"
34
+ RSpec::Core::RakeTask.new(:functional) do |t|
35
+ t.pattern = "spec/cuporter/functional/**/*_spec.rb"
36
+ t.spec_opts = ["--color" , "--format" , "doc" ]
37
+ end
38
+
39
+ desc "cucumber features"
40
+ task :cucumber do
41
+ sh "cucumber"
42
+ end
43
+ end
44
+
45
+ RDOC_OPTS = ["--all" , "--quiet" , "--line-numbers" , "--inline-source",
46
+ "--main", "README.textile",
47
+ "--title", "Cuporter: cucumber tag reporting"]
48
+ XTRA_RDOC = %w{README.textile LICENSE }
49
+
50
+ Rake::RDocTask.new do |rd|
51
+ rd.rdoc_dir = "doc/rdoc"
52
+ rd.rdoc_files.include("**/*.rb")
53
+ rd.rdoc_files.add(XTRA_RDOC)
54
+ rd.options = RDOC_OPTS
55
+ end
56
+
57
+ spec = Gem::Specification.new do |s|
58
+ s.name = 'cuporter'
59
+ s.version = '0.1.0'
60
+ s.rubyforge_project = s.name
61
+
62
+ s.platform = Gem::Platform::RUBY
63
+ s.has_rdoc = true
64
+ s.extra_rdoc_files = XTRA_RDOC
65
+ s.rdoc_options += RDOC_OPTS
66
+ s.summary = "Scrapes Cucumber *.feature files to build report on tag usage"
67
+ s.description = s.summary
68
+ s.author = "Tim Camper"
69
+ s.email = 'twcamper@thoughtworks.com'
70
+ s.homepage = 'http://github.com/twcamper/cuporter'
71
+ s.required_ruby_version = '>= 1.8.7'
72
+ s.default_executable = "cuporter"
73
+ s.executables = [s.default_executable]
74
+
75
+ s.files = %w(LICENSE README.textile Rakefile) +
76
+ FileList["lib/**/*.rb", "spec/**/*.rb", "features/**/*.{feature,rb}", "bin/*"].to_a
77
+
78
+ s.require_path = "lib"
79
+ end
80
+
81
+ Rake::GemPackageTask.new(spec) do |p|
82
+ p.need_zip = true
83
+ p.need_tar = true
84
+ p.gem_spec = spec
85
+ end
86
+ end
data/bin/cuporter ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+ require 'cuporter'
5
+
6
+ tag_report = Cuporter::TagReport.new(Cuporter::Options[:input_file] || Cuporter::Options[:input_dir])
7
+
8
+ formatter = Cuporter::Formatters.const_get(Cuporter::Options[:format])
9
+ formatter.new(tag_report.scenarios_per_tag, Cuporter::Options[:output]).write
@@ -0,0 +1,7 @@
1
+ Feature: Pretty print report on 3 features
2
+
3
+ @just_me
4
+ Scenario: Everything in fixtures/self_test
5
+ When I run cuporter --in fixtures/self_test
6
+ Then the output should have the same contents as "features/reports/pretty_with_outlines.txt"
7
+
@@ -0,0 +1,8 @@
1
+ When /^I run cuporter (.*)$/ do |cuporter_opts|
2
+ @output = `bin#{File::SEPARATOR}cuporter #{cuporter_opts}`
3
+ end
4
+
5
+
6
+ Then /^.* should have the same contents as "([^"]*)"$/ do |expected_file|
7
+ @output.should == IO.read(File.expand_path(expected_file))
8
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH << File.expand_path('../../../lib' , __FILE__)
2
+ require 'cuporter'
@@ -0,0 +1,46 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ require 'optparse'
3
+ require 'fileutils'
4
+
5
+ module Cuporter
6
+ class Options
7
+
8
+ def self.[](key)
9
+ self.options[key]
10
+ end
11
+
12
+ def self.options
13
+ self.parse unless @options
14
+ @options
15
+ end
16
+
17
+ def self.parse
18
+ @options = {}
19
+ OptionParser.new(ARGV.dup) do |opts|
20
+ opts.banner = "Usage: cuporter [options]\n\n"
21
+
22
+ opts.on("-i", "--in DIR", "directory of *.feature files\n\t\t\t\t\tDefault: features/**/*.feature\n\n") do |i|
23
+ @options[:input_dir] = "#{i}/**/*.feature"
24
+ end
25
+ opts.on("--input-file FILE", "full file name with extension: 'path/to/file.feature'\n\n") do |file|
26
+ @options[:input_file] = file
27
+ end
28
+ opts.on("-o", "--out FILE", "Output file path\n\n") do |o|
29
+ full_path = File.expand_path(o)
30
+ path = full_path.split(File::SEPARATOR)
31
+ file = path.pop
32
+ FileUtils.makedirs(path.join(File::SEPARATOR))
33
+
34
+ @options[:output] = full_path
35
+ end
36
+ opts.on("-f", "--format [pretty|html|csv]", "Output format\n\t\t\t\t\tDefault: pretty text\n\n") do |f|
37
+ @options[:format] = f.downcase.to_class_name
38
+ end
39
+ @options[:format] ||= :Text
40
+
41
+ end.parse!
42
+
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module StringExtensions
3
+ def to_class_name
4
+ gsub(/(^|_)([a-zA-Z])/) {$2.upcase}
5
+ end
6
+ end
7
+
8
+ class String
9
+ include(StringExtensions)
10
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+
3
+ module Cuporter
4
+ class FeatureParser
5
+ FEATURE_LINE = /^\s*(Feature:[^#]+)/
6
+ TAG_LINE = /^\s*(@\w.+)/
7
+ SCENARIO_LINE = /^\s*(Scenario:[^#]+)$/
8
+ SCENARIO_OUTLINE_LINE = /^\s*(Scenario Outline:[^#]+)$/
9
+ SCENARIOS_LINE = /^\s*(Scenarios:[^#]*)$/
10
+ EXAMPLES_LINE = /^\s*(Examples:[^#]*)$/
11
+
12
+ def initialize
13
+ @current_tags = []
14
+ end
15
+
16
+ def self.parse(feature_content)
17
+ self.new.parse(feature_content)
18
+ end
19
+
20
+ def parse(feature_content)
21
+ lines = feature_content.split(/\n/)
22
+
23
+ lines.each do |line|
24
+ case line
25
+ when TAG_LINE
26
+ # may be more than one tag line
27
+ @current_tags |= $1.strip.split(/\s+/)
28
+ when FEATURE_LINE
29
+ @feature = TagListNode.new($1.strip, @current_tags)
30
+ @current_tags = []
31
+ when SCENARIO_LINE
32
+ # How do we know when we have read all the lines from a "Scenario Outline:"?
33
+ # One way is when we encounter a "Scenario:"
34
+ if @scenario_outline
35
+ @feature.merge(@scenario_outline)
36
+ @scenario_outline = nil
37
+ end
38
+
39
+ @feature.add_to_tag_node(Node.new($1.strip), @current_tags)
40
+ @current_tags = []
41
+ when SCENARIO_OUTLINE_LINE
42
+ # ... another is when we hit a subsequent "Scenario Outline:"
43
+ if @scenario_outline
44
+ @feature.merge(@scenario_outline)
45
+ @scenario_outline = nil
46
+ end
47
+
48
+ @scenario_outline = TagListNode.new($1.strip, @current_tags)
49
+ @current_tags = []
50
+ when EXAMPLES_LINE, SCENARIOS_LINE
51
+ @scenario_outline.add_to_tag_node(Node.new($1.strip), @feature.universal_tags | @current_tags)
52
+ @current_tags = []
53
+ end
54
+ end
55
+
56
+ # EOF is the final way that we know we are finished with a "Scenario Outline"
57
+ @feature.merge(@scenario_outline) if @scenario_outline
58
+ return @feature
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,11 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module Cuporter
3
+ module Formatters
4
+ class Csv < Writer
5
+ include TextMethods
6
+
7
+ TAB = ","
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ require 'rubygems'
3
+ require 'erb'
4
+ require 'builder'
5
+
6
+ module Cuporter
7
+ module Formatters
8
+ class Html < Writer
9
+
10
+ NODE_CLASS = [:tag, :feature, :scenario, :example]
11
+
12
+ def write_nodes
13
+ @report.children.sort.each do |tag_node|
14
+ write_node(tag_node, 0)
15
+ end
16
+ builder
17
+ end
18
+
19
+ def builder
20
+ @builder ||= Builder::XmlMarkup.new
21
+ end
22
+
23
+ def write_node(node, indent_level)
24
+ builder.li do |list_item|
25
+ list_item.span(node.name, :class => NODE_CLASS[indent_level])
26
+ if node.has_children?
27
+ list_item.ul do |list|
28
+ node.children.sort.each do |child|
29
+ if child.has_children?
30
+ write_node(child, indent_level + 1)
31
+ else
32
+ list.li(child.name, :class => NODE_CLASS[indent_level + 1])
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def get_binding
41
+ binding
42
+ end
43
+
44
+ def rhtml
45
+ ERB.new(RHTML)
46
+ end
47
+
48
+ def write
49
+ @output.puts rhtml.result(get_binding).reject {|line| /^\s+$/ =~ line}
50
+ end
51
+
52
+ RHTML = %{
53
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
54
+ <html xmlns="http://www.w3.org/1999/xhtml">
55
+ <head>
56
+ <title>Cucumber Tags</title>
57
+ <style type="text/css"></style>
58
+ </head>
59
+ <body>
60
+ <ul>
61
+ <%= write_nodes%>
62
+ </ul>
63
+ </body>
64
+ </html>
65
+ }
66
+
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,11 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module Cuporter
3
+ module Formatters
4
+ class Text < Writer
5
+ include TextMethods
6
+
7
+ TAB = " "
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module Cuporter
3
+ module Formatters
4
+ module TextMethods
5
+
6
+ def write
7
+ @report.children.sort.each do |tag_node|
8
+ write_node(tag_node, 0)
9
+ end
10
+ end
11
+
12
+ def write_node(node, tab_stops)
13
+ @output.puts "#{self.class::TAB * tab_stops}#{node.name}"
14
+ node.children.sort.each do |child|
15
+ @output.puts "#{self.class::TAB + (self.class::TAB * tab_stops)}#{child.name}"
16
+ child.children.sort.each do |grand_child|
17
+ if grand_child.has_children?
18
+ write_node(grand_child, tab_stops + 2)
19
+ else
20
+ @output.puts "#{self.class::TAB * tab_stops}#{self.class::TAB * 2}#{grand_child.name}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+
3
+ module Cuporter
4
+ module Formatters
5
+ class Writer
6
+
7
+ def initialize(report, output)
8
+ @report = report
9
+ if output
10
+ @output = File.open(output, "w")
11
+ else
12
+ @output = STDOUT
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module Cuporter
3
+ class Node
4
+ include Comparable
5
+
6
+ attr_reader :name, :children
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @children = []
11
+ end
12
+
13
+ def has_children?
14
+ @children.size > 0
15
+ end
16
+
17
+ # will not add duplicate
18
+ def add_child(node)
19
+ @children << node unless has_child?(node)
20
+ end
21
+
22
+ def find_or_create_child(name)
23
+ child_node = self[name]
24
+ unless child_node
25
+ children << Node.new(name)
26
+ child_node = children.last
27
+ end
28
+ child_node
29
+ end
30
+
31
+ def find_by_name(name)
32
+ children.find {|c| c.name == name.to_s}
33
+ end
34
+ alias :[] :find_by_name
35
+
36
+ def find(node)
37
+ children.find {|c| c == node}
38
+ end
39
+ alias :has_child? :find
40
+
41
+ def name_without_title
42
+ @name_without_title ||= name.split(/:\s+/).last
43
+ end
44
+
45
+ # sort on name or substring of name after any ':'
46
+ def <=>(other)
47
+ name_without_title <=> other.name_without_title
48
+ end
49
+
50
+ # value equivalence
51
+ def eql?(other)
52
+ name == other.name && children == other.children
53
+ end
54
+ alias :== :eql?
55
+
56
+ # Have my children adopt the other node's grandchildren.
57
+ #
58
+ # Copy children of other node's top-level, direct descendants to this
59
+ # node's direct descendants of the same name.
60
+ def merge(other)
61
+ other.children.each do |other_child|
62
+ direct_child = find_or_create_child(other_child.name)
63
+ new_grandchild = Node.new(other.name)
64
+ other_child.children.collect do |c|
65
+ new_grandchild.add_child(c)
66
+ end
67
+ direct_child.add_child(new_grandchild)
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module Cuporter
3
+ # a node with a list of tags that apply to all children
4
+ class TagListNode < Node
5
+
6
+ attr_reader :universal_tags
7
+
8
+ def initialize(name, universal_tags)
9
+ super(name)
10
+ @universal_tags = universal_tags
11
+ end
12
+
13
+ def has_universal_tags?
14
+ @universal_tags.size > 0
15
+ end
16
+
17
+ def add_to_tag_node(node, childs_tags = [])
18
+ (universal_tags | childs_tags).each do |tag|
19
+ tag_node = find_or_create_child(tag)
20
+ tag_node.add_child(node)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ module Cuporter
3
+ class TagReport
4
+
5
+ def initialize(input_file_pattern)
6
+ @input_file_pattern = input_file_pattern || "features/**/*.feature"
7
+ end
8
+
9
+ def files
10
+ Dir[@input_file_pattern].collect {|f| File.expand_path f}
11
+ end
12
+
13
+ def scenarios_per_tag
14
+ tags = TagListNode.new("report",[])
15
+ files.each do |file|
16
+ content = File.read(file)
17
+ tags.merge(FeatureParser.parse(content)) unless content.empty?
18
+ end
19
+ tags
20
+ end
21
+
22
+ end
23
+ end
data/lib/cuporter.rb ADDED
@@ -0,0 +1,12 @@
1
+ # Copyright 2010 ThoughtWorks, Inc. Licensed under the MIT License
2
+ require 'cuporter/node'
3
+ require 'cuporter/tag_list_node'
4
+ require 'cuporter/feature_parser'
5
+ require 'cuporter/extensions/string'
6
+ require 'cuporter/cli/options'
7
+ require 'cuporter/tag_report'
8
+ require 'cuporter/formatters/writer'
9
+ require 'cuporter/formatters/text_methods'
10
+ require 'cuporter/formatters/text'
11
+ require 'cuporter/formatters/csv'
12
+ require 'cuporter/formatters/html'
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ module Cuporter
4
+ describe FeatureParser do
5
+ context "#universal_tags" do
6
+ context "one tag" do
7
+ it "returns one tag" do
8
+ feature = FeatureParser.parse("@wip\nFeature: foo")
9
+ feature.universal_tags.should == ["@wip"]
10
+ end
11
+ end
12
+
13
+ context "two tags on one line" do
14
+ it "returns two tags" do
15
+ feature = FeatureParser.parse(" \n@smoke @wip\nFeature: foo")
16
+ feature.universal_tags.sort.should == %w[@smoke @wip].sort
17
+ end
18
+ end
19
+ context "two tags on two lines" do
20
+ it "returns two tags" do
21
+ feature = FeatureParser.parse(" \n@smoke\n @wip\nFeature: foo")
22
+ feature.universal_tags.sort.should == %w[@smoke @wip].sort
23
+ end
24
+ end
25
+ context "no tags" do
26
+ it "returns no tags" do
27
+ feature = FeatureParser.parse("\nFeature: foo")
28
+ feature.universal_tags.should == []
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ context "#name" do
35
+ let(:name) {"Feature: consume a fairly typical feature name, and barf it back up"}
36
+ context "sentence with comma" do
37
+ it "returns the full name" do
38
+ feature = FeatureParser.parse("\n#{name}\n Background: blah")
39
+ feature.name.should == name
40
+ end
41
+ end
42
+ context "name followed by comment" do
43
+ it "returns only the full name" do
44
+ feature = FeatureParser.parse("# Here is a feature comment\n# And another comment\n #{name} # comment text here\n Background: blah")
45
+ feature.name.should == name
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
53
+