class OrgConverge::Command

Attributes

dotorg[R]
engine[R]
logger[R]
ob[R]
runmode[R]

Public Class Methods

new(options) click to toggle source
# File lib/org-converge/command.rb, line 10
def initialize(options)
  @options   = options
  @dotorg    = options['<org_file>']
  @root_dir  = options['--root-dir']
  @dir       = options['--dir']
  @run_dir   = if @root_dir
                 File.expand_path(File.join(@root_dir, 'run'))
               else
                 File.expand_path('run')
               end
  # The results dir will have a timestamp to avoid having to refresh all the time
  results_dirname = "results_#{Time.now.strftime("%Y%m%d%H%M%S")}"
  @results_dir = if @root_dir
                   File.expand_path(File.join(@root_dir, results_dirname))
                 else
                   File.expand_path(results_dirname)
                 end
  @ob    = Orgmode::Parser.new(File.read(dotorg)).babelize
  @babel = nil
  @logger  = Logger.new(options['--log'] || STDOUT)
  logger.formatter = proc do |severity, datetime, progname, msg| 
    "[#{datetime.strftime('%Y-%m-%dT%H:%M:%S %z')}] #{msg}\n"
  end

  # Keep track of the exit status from the process for idempotency checks
  @procs_exit_status = Hash.new { |h,k| h[k] = { } }
end

Public Instance Methods

converge!() click to toggle source
# File lib/org-converge/command.rb, line 55
def converge!
  tangle!
  @runmode = @options['--runmode'] || ob.in_buffer_settings['RUNMODE']
  case
  when @options['--name']
    if runmode == 'sequentially'
      run_matching_blocks_sequentially!
    else
      run_matching_blocks!
    end
  else
    dispatch_runmode(runmode)
  end
end
dispatch_runmode(runmode) click to toggle source
# File lib/org-converge/command.rb, line 70
def dispatch_runmode(runmode)
  case runmode
  when 'parallel'
    run_blocks_in_parallel!
  when 'sequential', 'idempotent'
    run_blocks_sequentially!
  when 'chained', 'chain', 'tasks'
    run_blocks_chain!
  when 'spec'
    run_against_blocks_results!
  else # parallel by default
    run_blocks_in_parallel!
  end
end
execute!() click to toggle source
# File lib/org-converge/command.rb, line 38
def execute!
  case
  when @options['--showfiles']
    showfiles
  when @options['--tangle']
    tangle!
  else
    converge!
  end

  true
rescue => e
  @logger.error e
  @logger.error e.backtrace.join("\n")
  false
end
run_against_blocks_results!() click to toggle source
# File lib/org-converge/command.rb, line 223
def run_against_blocks_results!
  require 'diff/lcs'
  require 'diff/lcs/hunk'

  succeeded = []
  failed    = []

  logger.info "Runmode: spec"
  runlist_stack = []
  scripts = if @options['--name']
              babel.ob.scripts.select {|k, h| h[:header][:name] =~ Regexp.new(@options['--name']) }
            else
              babel.ob.scripts
            end
  scripts.each { |key, script| runlist_stack << [key, script] }

  babel.tangle_runnable_blocks!(@run_dir)
  FileUtils.mkdir_p(@results_dir)

  while not runlist_stack.empty?
    key, script = runlist_stack.shift

    # Decision: Only run blocks which have a name
    next unless script[:header][:name]

    display_name = script[:header][:name]
    script_file  = File.expand_path("#{@run_dir}/#{key}")
    results_file = File.expand_path("#{@results_dir}/#{key}")
    bin = determine_lang_bin(script)
    cmd = "#{bin} #{script_file}"

    with_running_engine(:runmode => 'spec', :results_dir => @results_dir) \
    do |engine|
      engine.register display_name, cmd, { 
        :cwd     => @root_dir, 
        :logger  => logger,
        :results => results_file
      }
    end

    if scripts[:results]
      print "Checking results from '#{display_name.fg 'yellow'}' code block:\t"
      expected_lines = script[:results].split("\n").map! {|e| e.chomp }
      actual_lines   = File.open(results_file).read.split("\n").map! {|e| e.chomp }

      output_diff = diff(expected_lines, actual_lines)
      if output_diff.empty?
        puts "OK".fg 'green'
        succeeded << display_name
      else
        puts "DIFF".fg 'red'
        puts output_diff.fg 'red'
        failed << display_name
      end
    end
  end

  if failed.count > 0
    puts ''
    puts 'Failed code blocks:'.fg 'red'
    failed.each do |name|
      puts "  - #{name.fg 'yellow'}"
    end
    puts ''
  end

  puts "#{succeeded.count + failed.count} examples, #{failed.count} failures".fg 'green'
  exit 1 if failed.count > 0
end
run_blocks_chain!() click to toggle source
# File lib/org-converge/command.rb, line 91
def run_blocks_chain!      
  # Chain the blocks by defining them as Rake::Tasks dynamically
  tasks = { }

  babel.tangle_runnable_blocks!(@run_dir)
  babel.ob.scripts.each do |key, script|
    task_name = script[:header][:name]
    next unless task_name

    task = Rake::Task.define_task task_name do
      with_running_engine do |engine|
        file = File.expand_path("#{@run_dir}/#{key}")
        bin = determine_lang_bin(script)
        cmd = "#{bin} #{file}"
        run_procs(script, cmd, engine)
      end
    end
    tasks[task_name] = { 
      :task => task,
      :script => script
    }
  end

  # Now onto define the prerequisites and actions
  tasks.each_pair do |task_name, task_definition|
    prerequisite_task = task_definition[:script][:header][:after]
    if prerequisite_task and tasks[prerequisite_task]
      task_definition[:task].prerequisites << tasks[prerequisite_task][:task]
    end

    postrequisite_task = task_definition[:script][:header][:before]
    if postrequisite_task and tasks[postrequisite_task]
      tasks[postrequisite_task][:task].prerequisites << task_definition[:task]
    end
  end

  # The task that marks the run as done needs to be defined explicitly
  # otherwise a block named default will tried to be run
  final_task = babel.ob.in_buffer_settings['FINAL_TASK'] || 'default'

  if tasks[final_task]
    logger.info "Running final task: #{tasks[final_task][:task]}"
    tasks[final_task][:task].invoke
  else
    logger.error "Could not find a final task to run!"
  end
end
run_blocks_in_parallel!() click to toggle source
# File lib/org-converge/command.rb, line 164
def run_blocks_in_parallel!
  @engine = OrgConverge::Engine.new(:logger => @logger, :babel => @babel)
  babel.tangle_runnable_blocks!(@run_dir)
  babel.ob.scripts.each do |key, script|
    # Decision: Only run blocks which have a name
    next unless script[:header][:name]

    file = File.expand_path("#{@run_dir}/#{key}")
    bin = determine_lang_bin(script)
    cmd = "#{bin} #{file}"
    run_procs(script, cmd)
  end
  logger.info "Running code blocks now! (#{babel.ob.scripts.count} runnable blocks found in total)"
  @engine.start
  logger.info "Run has completed successfully.".fg 'green'
end
run_blocks_sequentially!() click to toggle source
# File lib/org-converge/command.rb, line 139
def run_blocks_sequentially!
  babel.tangle_runnable_blocks!(@run_dir)

  runlist_stack = []
  babel.ob.scripts.each do |key, script|
    runlist_stack << [key, script]
  end

  while not runlist_stack.empty?
    key, script = runlist_stack.shift
    # Decision: Only run blocks which have a name
    next unless script[:header][:name]
    display_name = script[:header][:name]
    exit_status_list = with_running_engine(:runmode => @runmode) \
    do |engine|
      file = File.expand_path("#{@run_dir}/#{key}")
      bin = determine_lang_bin(script)
      cmd = "#{bin} #{file}"
      run_procs(script, cmd, engine)
    end
    @procs_exit_status.merge!(exit_status_list)
  end
  logger.info "Run has completed successfully.".fg 'green'
end
run_matching_blocks!() click to toggle source
# File lib/org-converge/command.rb, line 181
def run_matching_blocks!
  @engine = OrgConverge::Engine.new(:logger => @logger, :babel => @babel)
  babel.tangle_runnable_blocks!(@run_dir, :filter => @options['--name'])
  scripts = babel.ob.scripts.select {|k, h| h[:header][:name] =~ Regexp.new(@options['--name']) }
  scripts.each do |key, script|
    file = File.expand_path("#{@run_dir}/#{key}")
    bin = determine_lang_bin(script)
    cmd = "#{bin} #{file}"
    run_procs(script, cmd)
  end

  logger.info "Running code blocks now! (#{scripts.count} runnable blocks found in total)"
  @engine.start
  logger.info "Run has completed successfully.".fg 'green'
end
run_matching_blocks_sequentially!() click to toggle source
# File lib/org-converge/command.rb, line 197
def run_matching_blocks_sequentially!
  babel.tangle_runnable_blocks!(@run_dir)

  runlist_stack = []
  scripts = babel.ob.scripts.select {|k, h| h[:header][:name] =~ Regexp.new(@options['--name']) }
  scripts.each do |key, script|
    runlist_stack << [key, script]
  end

  while not runlist_stack.empty?
    key, script = runlist_stack.shift

    # Decision: Only run blocks which have a name
    next unless script[:header][:name]

    display_name = script[:header][:name]
    with_running_engine do |engine|
      file = File.expand_path("#{@run_dir}/#{key}")
      bin = determine_lang_bin(script)
      cmd = "#{bin} #{file}"
      run_procs(script, cmd, engine)
    end
  end
  logger.info "Run has completed successfully.".fg 'green'
end
tangle!() click to toggle source
# File lib/org-converge/command.rb, line 85
def tangle!
  results = babel.tangle!
rescue Orgmode::Babel::TangleError
  logger.error "Cannot converge because there were errors during tangle step".fg 'red'
end

Private Instance Methods

babel() click to toggle source
# File lib/org-converge/command.rb, line 333
def babel
  @babel ||= Orgmode::Babel.new(ob, { :logger => @logger, :root_dir => @root_dir })
end
diff(expected_lines, actual_lines) click to toggle source
# File lib/org-converge/command.rb, line 294
def diff(expected_lines, actual_lines)
  output = ""
  file_length_difference = 0

  diffs = Diff::LCS.diff(expected_lines, actual_lines)
  hunks = diffs.map do |piece|
    Diff::LCS::Hunk.new(
                        expected_lines, actual_lines, piece, 3, 0
                        ).tap do |h|
      file_length_difference = h.file_length_difference 
    end
  end

  hunks.each_cons(2) do |prev_hunk, current_hunk|
    begin
      if current_hunk.overlaps?(prev_hunk)
        current_hunk.merge(prev_hunk)
      else
        output << prev_hunk.diff(:unified).to_s
      end
    rescue => e
    end
  end

  if hunks.last
    output << hunks.last.diff(:unified).to_s
  end

  output
end
run_procs(script, cmd, engine=nil) click to toggle source
# File lib/org-converge/command.rb, line 349
def run_procs(script, cmd, engine=nil)
  engine ||= @engine
  display_name = script[:header][:name]

  if @runmode == 'idempotent'
    case
    when script[:header][:if]
      block_name = script[:header][:if]
      exit_status = @procs_exit_status[block_name]
      unless exit_status == 0
        logger.info "#{display_name.fg 'green'} -- Skipped since :if clause matches check from '#{block_name.fg 'yellow'}'"
        return
      end
    when script[:header][:unless]
      block_name = script[:header][:unless]
      exit_status = @procs_exit_status[block_name]
      if exit_status == 0
        logger.info "#{display_name.fg 'green'} -- Skipped since :unless clause matches check from '#{block_name.fg 'yellow'}'"
        return
      end
    end
  end

  script[:header][:dir] = @dir if @dir
  if script[:header][:procs]
    procs = script[:header][:procs].to_i
    1.upto(procs) do |i|
      proc_name = "#{display_name}:#{i}"
      engine.register proc_name, cmd, { :cwd => @root_dir, :logger => logger,  :header => script[:header] }
    end
  else
    engine.register display_name, cmd, { :cwd => @root_dir, :logger => logger, :header => script[:header] }
  end
end
showfiles() click to toggle source
# File lib/org-converge/command.rb, line 337
def showfiles
  ob.tangle.each do |file, block|
    puts "---------- #{file} --------------".fg 'green'
    puts block[:lines]
  end

  ob.scripts.each do |index, block|
    puts "---------- script: #{index} to be run with: #{block[:header][:shebang]} --------------".fg 'green'
    puts block[:lines]
  end
end
with_running_engine(opts={}) { |engine| ... } click to toggle source
# File lib/org-converge/command.rb, line 325
def with_running_engine(opts={})
  default_options = { :logger => @logger, :babel => babel }
  options = default_options.merge!(opts)
  engine = OrgConverge::Engine.new(options)
  yield engine
  engine.start
end