Skip to content

Instantly share code, notes, and snippets.

@kornysietsma
Created April 14, 2009 07:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kornysietsma/95033 to your computer and use it in GitHub Desktop.
Save kornysietsma/95033 to your computer and use it in GitHub Desktop.
require 'rubygems'
require 'cucumber'
require 'xmlrpc/client'
# a hacky way to combine outputs to several io devices
class IoMerger
attr_accessor :other_ios
def initialize(base_io)
@base_io = base_io
@other_ios = {} # name based hash so we can add/remove ios by name
end
def add_io(name, io)
@other_ios[name] = io
end
def remove_io(name)
raise "No such io entry: #{name}" unless @other_ios.has_key? name
@other_ios.delete name
end
def remove_all_but_base
@other_ios = {}
end
def method_missing(method, *args)
@base_io.send(method, args)
@other_ios.values.each {|io| io.send(method,args)}
end
end
class WikiUpdater
def initialize(io)
# TODO: put these things in a config file somewhere:
@config = {
:wiki_base => "http://wiki_host",
:wiki_space => "MYPROJ",
:features_parent => "Features",
:story_prefix => ""
}
@server = XMLRPC::Client.new2("#{@config[:wiki_base]}/rpc/xmlrpc")
@io = io
@token = server_call("confluence1.login", "user", "password")
@all_pages = server_call("confluence1.getPages",@token,@config[:wiki_space])
end
def server_call(function, *args)
begin
@server.call(function, *args)
rescue XMLRPC::FaultException => e
errmsg = "XMLRPC fault code #{e.faultCode} - '#{e.faultString}"
@io.puts errmsg
$stderr.puts errmsg
raise
end
end
def logout()
server_call("confluence1.logout", @token)
end
def getPage(page)
server_call("confluence1.getPage", @token, @config[:wiki_space],page)
end
def getPagePermissions(page_id)
server_call("confluence1.getPagePermissions", @token, page_id)
end
def storePage(page)
server_call("confluence1.storePage", @token, page)
end
def getChildren(page_id)
server_call("confluence1.getChildren", @token, page_id)
end
# update a feature page in the wiki - pretty straightforward:
def update_feature(feature_name, featuretext)
@io.puts "Saving feature '#{feature_name}' to wiki"
feature_parent = getPage @config[:features_parent]
feature_parent_id = feature_parent["id"]
found_page = @all_pages.detect {|pageInfo| pageInfo["title"].strip.downcase == feature_name.strip.downcase}
if found_page
page_name = found_page["title"] # might differ in case or something from target page
@io.puts " found matching page '#{page_name}'- overwriting"
feature_page = getPage(page_name) # re-get it in case of changes
raise "Page #{feature_name} manually changed!!!" unless feature_page["content"].include? "this page is automatically generated"
new_content = feature_page_wrap(feature_name, featuretext)
if new_content == feature_page["content"]
@io.puts " No changes - nothing updated"
else
feature_page["content"] = new_content
featurepage_res = storePage feature_page
@io.puts " Saved page #{feature_page["title"]} with id #{featurepage_res["id"]}"
end
else
@io.puts " no match - creating page"
feature_page = {
"space" => @config[:wiki_space],
"parentId" => feature_parent_id,
"title" => feature_name,
"content" => feature_page_wrap(feature_name, featuretext),
}
featurepage_res = storePage feature_page
@io.puts " Saved new page #{feature_page["title"]} with id #{featurepage_res["id"]}"
end
end
# updates a story page in the wiki
# uses anchors (if present) for positioning each section (qa, acceptance and other):
# {anchor:<name>_features} to {anchor:end_<name>_features}
def update_story(story_name, scenario_list)
@io.puts "updating story #{story_name} against #{scenario_list.size} scenarios"
page_name = @config[:story_prefix] + story_name.strip.downcase
found_page = @all_pages.detect {|pageInfo| pageInfo["title"].strip.downcase == page_name}
unless found_page
@io.puts "No wiki page found matching '#{page_name}' - nothing to do! (script does not currently create pages automatically)"
return
end
story_page = getPage(found_page["title"])
original_content = story_page["content"].dup
# for each scenario, group by section : 'qa', 'acceptance' or 'other
# and then pump out features in alphabetical order in each section
sections = {
:qa => {
:text => story_section_header("Cucumber scenarios: QA"),
:features => {} # feature name => list of scenarios
},
:acceptance => {
:text => story_section_header("Cucumber scenarios: Acceptance"),
:features => {} # feature name => list of scenarios
},
:other => {
:text => story_section_header("Cucumber scenarios: Other"),
:features => {} # feature name => list of scenarios
},
}
scenario_list.each do |data|
section = :other
section = :qa if data[:tags].include? "qa"
section = :acceptance if data[:tags].include? "acceptance"
feature_hash = sections[section][:features]
scenario_text_list = (feature_hash[data[:feature]] ||= [])
scenario_text_list << data[:io].string
end
sections.each do |section_name, data|
if data[:features].empty? # want to blank out empty sections if they already exist
if has_section(story_page, section_name)
set_section(story_page, section_name, "")
end
next # skip further processing
end
@io.puts "updating page '#{story_page["title"]}' section '#{section_name}' with scenarios from #{data[:features].size} feature files."
data[:features].keys.sort.each do |feature_name|
data[:text] << story_feature_header(feature_name)
data[:features][feature_name].each do |scenario_text|
data[:text] << scenario_text
end
data[:text] << story_feature_footer
end
# end of section - add footer, update page
data[:text] << story_section_footer
unless has_section(story_page, section_name)
append_section(story_page, section_name)
end
set_section(story_page, section_name, data[:text])
end
if original_content == story_page["content"]
@io.puts "No changes to page #{story_page["title"]}- nothing to do"
else
storypage_res = storePage story_page
@io.puts " Saved updated page #{story_page["title"]} with id #{storypage_res["id"]}"
end
end
def feature_page_wrap(feature_name, featuretext)
<<-EOT
h2. Feature #{feature_name}
_Note this page is automatically generated - change by altering features and tags in subversion only!_
{toc:minLevel=3}
----
#{featuretext}
----
EOT
end
def section_start(section)
"{anchor:#{section}_features}"
end
def section_end(section)
"{anchor:end_#{section}_features}"
end
def has_section(page, section)
has_start_anchor = page["content"].include? section_start(section)
has_end_anchor = page["content"].include? section_end(section)
if has_start_anchor != has_end_anchor
raise "Page #{page["title"]} has mismatched anchor tags for section #{section}"
end
return has_start_anchor
end
def append_section(page, section)
page["content"] << "\n#{section_start(section)}\n#{section_end(section)}\n"
end
def set_section(page, section, text)
section_pattern = /(#{Regexp.escape(section_start(section))}).*(#{Regexp.escape(section_end(section))})/m
page["content"].gsub!(section_pattern, "\\1\n#{text}\n\\2")
end
def story_section_header(title)
"{panel:title=#{title}|borderStyle=solid|titleBGColor=#FFFFCE|bgColor=#FFFFFF}\n"
end
def story_section_footer
"{panel}"
end
def story_feature_header(feature_name)
found_page = @all_pages.detect {|pageInfo| pageInfo["title"].strip.downcase == feature_name.strip.downcase}
feature_text = found_page ? "[#{found_page["title"]}]" : "'#{feature_name}'"
"??from feature #{feature_text}??\n"
end
def story_feature_footer
"\n----\n"
end
end
# wiki formatter loosely based on html formatter, but
# outputs to confluence wiki format
class WikiFormatter < Cucumber::Ast::Visitor
def initialize(step_mother, io, options)
super(step_mother)
@io = io
@full_mode = options[:dry_run]
@stories = {} # hash of story name => list of {:feature => 'feature name', :scenario => scenario name, :tags => [tags], :io => 'story wiki text'}
@features = {} # hash of feature file => list of feature dumps
end
def visit_features(features)
super
# after the features are all process, send output to the wiki
wiki_updater = WikiUpdater.new(@io)
begin
@io.puts "Finished #{@features.size} features."
@features.each do |fname, fio|
wiki_updater.update_feature(fname, fio.string)
end
@io.puts "#{@stories.size} stories found:"
@stories.each do |sname, slist|
wiki_updater.update_story(sname, slist)
end
@io.puts " - That's all, folks!"
ensure
wiki_updater.logout
end
end
def visit_feature(feature)
@current_feature_name = File.basename(feature.file, ".feature")
raise "Duplicate feature: #{@current_feature_name}" if @features.has_key? @current_feature_name
@current_feature = StringIO.new
@features[@current_feature_name] = @current_feature
@current_output = IoMerger.new(@current_feature)
@current_tags = []
super
@io.puts "Finished feature #{@current_feature_name}"
end
def visit_comment_line(comment_line)
unless comment_line.blank?
@current_output.puts "_#{comment_line}_"
end
end
def is_story(tag_name)
tag_name =~ /^story-/
end
def story_from_tag(tag_name)
/^story-(.*)$/.match(tag_name)[1].downcase
end
def visit_tag_name(tag_name)
# always convert tags to lower case, to avoid accidental mis-namings
@current_tags << tag_name.downcase
end
def visit_feature_name(name)
lines = name.split(/\r?\n/)
@current_output.puts "h4. #{lines[0]}"
if lines.size > 1
lines[1..-1].each do |line|
@current_output.puts line
end
end
end
def visit_background_name(keyword, name, file_colon_line, source_indent)
@current_output.puts "h4. #{keyword} #{name}"
end
# a feature element is a scenario or a scenario outline
def visit_feature_element(feature_element)
# reset tags before each scenario/outline
@current_tags = []
super
# remove any output targets apart from main feature page output
@current_output.remove_all_but_base
end
def visit_scenario_name(keyword, name, file_colon_line, source_indent)
@current_tags.each do |tag_name|
if is_story(tag_name)
story_name = story_from_tag(tag_name)
story = StringIO.new
@current_output.add_io(story_name, story)
(@stories[story_name] ||= []) << {:feature => @current_feature_name, :scenario => "#{keyword} #{name}", :tags => @current_tags.dup, :io => story }
end
end
tag_text = @current_tags.collect{|t| "*@#{t}*"}.join(' ')
@current_output.puts "h5. #{keyword} #{name}"
@current_output.puts "#{tag_text}" if @current_tags.size > 0
end
def visit_examples_name(keyword, name)
# start of an examples section - new table assumed.
# Note: not really tested with step tables or scenario tables...
@top_table_row = true
@current_output.puts "h6. #{keyword} #{name}"
end
def visit_table_row(table_row)
@current_output.print @top_table_row ? "|| " : "| "
super
@current_output.puts
@top_table_row = false
end
def visit_table_cell(table_cell)
super
end
def visit_table_cell_value(value, width, status)
@current_output.print value.gsub("|",":") + (@top_table_row ? " || " : " | ")
end
def visit_steps(steps)
steps.accept(self)
@current_output.puts # need a blank line to fix confluence bullet indents
end
def visit_step_name(keyword, step_match, status, source_indent, background)
step_name = step_match.format_args(lambda{|param| "+#{param}+"})
indent = keyword == "And" ? "**" : "*"
if status == :undefined # pending steps
@current_output.puts "#{indent} ??*#{keyword}* #{step_name}??"
else
@current_output.puts "#{indent} *#{keyword}* #{step_name}"
end
end
# shouldn't need to handle exceptions as we don't run code:
# def visit_exception(exception, status)
# no support for multiline arguments yet:
# def visit_multiline_arg(multiline_arg)
# no support for triple-quote strings yet:
# def visit_py_string(string, status)
# announcements are inserted by step code - we don't run steps, so no need to handle:
# def announce(announcement)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment