Created
April 14, 2009 07:10
-
-
Save kornysietsma/95033 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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