cukeq 0.0.1.dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +1 -0
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +73 -0
- data/Rakefile +68 -0
- data/VERSION +1 -0
- data/bin/cukeq +34 -0
- data/features/cukeq.feature +15 -0
- data/features/example1.feature +1 -0
- data/features/example2.feature +1 -0
- data/features/step_definitions/cukeq_steps.rb +22 -0
- data/features/support/cukeq_helper.rb +73 -0
- data/features/support/env.rb +13 -0
- data/features/support/report_app.rb +36 -0
- data/lib/cukeq.rb +45 -0
- data/lib/cukeq/broker.rb +65 -0
- data/lib/cukeq/em/system3.rb +53 -0
- data/lib/cukeq/job_clearer.rb +19 -0
- data/lib/cukeq/master.rb +146 -0
- data/lib/cukeq/reporter.rb +22 -0
- data/lib/cukeq/runner.rb +63 -0
- data/lib/cukeq/scenario_exploder.rb +32 -0
- data/lib/cukeq/scenario_runner.rb +134 -0
- data/lib/cukeq/scm.rb +51 -0
- data/lib/cukeq/scm/git_bridge.rb +36 -0
- data/lib/cukeq/scm/shell_svn_bridge.rb +65 -0
- data/lib/cukeq/scm/svn_bridge.rb +77 -0
- data/lib/cukeq/slave.rb +114 -0
- data/lib/cukeq/webapp.rb +38 -0
- data/spec/cukeq/broker_spec.rb +78 -0
- data/spec/cukeq/cukeq_spec.rb +10 -0
- data/spec/cukeq/master_spec.rb +153 -0
- data/spec/cukeq/reporter_spec.rb +29 -0
- data/spec/cukeq/runner_spec.rb +11 -0
- data/spec/cukeq/scenario_exploder_spec.rb +17 -0
- data/spec/cukeq/scenario_runner_spec.rb +32 -0
- data/spec/cukeq/scm/git_bridge_spec.rb +52 -0
- data/spec/cukeq/scm/svn_bridge_spec.rb +5 -0
- data/spec/cukeq/scm_spec.rb +48 -0
- data/spec/cukeq/slave_spec.rb +86 -0
- data/spec/cukeq/webapp_spec.rb +37 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- metadata +228 -0
    
        data/lib/cukeq/broker.rb
    ADDED
    
    | @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            module CukeQ
         | 
| 2 | 
            +
              class Broker
         | 
| 3 | 
            +
                attr_reader :user, :pass, :host, :port, :vhost
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(uri, opts = {})
         | 
| 6 | 
            +
                  uri = URI.parse(uri) if uri.kind_of? String
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  @user    = uri.user     || raise(ArgumentError, "no user given")
         | 
| 9 | 
            +
                  @pass    = uri.password || 'cukeq123'
         | 
| 10 | 
            +
                  @host    = uri.host     || 'localhost'
         | 
| 11 | 
            +
                  @port    = uri.port     || 5672
         | 
| 12 | 
            +
                  @vhost   = uri.path     || '/cukeq'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  @timeout = Integer(opts[:timeout] || 20)
         | 
| 15 | 
            +
                  @queues  = {}
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def start
         | 
| 19 | 
            +
                  log self.class, :start, "#{@host}:#{@port}#{@vhost}"
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  AMQP.start(amqp_options) do
         | 
| 22 | 
            +
                    create_queues
         | 
| 23 | 
            +
                    yield if block_given?
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def publish(queue_name, json)
         | 
| 28 | 
            +
                  queue_for(queue_name).publish(json)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def subscribe(queue_name, &blk)
         | 
| 32 | 
            +
                  queue_for(queue_name).subscribe(&blk)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def unsubscribe(queue_name, &blk)
         | 
| 36 | 
            +
                  queue_for(queue_name).unsubscribe(&blk)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def queue_for(name)
         | 
| 40 | 
            +
                  @queues[name] || raise("unknown queue: #{name.inspect}")
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                private
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def create_queues
         | 
| 46 | 
            +
                  @queues[:ping]    = MQ.new.queue("cukeq.ping")
         | 
| 47 | 
            +
                  @queues[:pong]    = MQ.new.queue("cukeq.pong")
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  @queues[:results] = MQ.new.queue("cukeq.results")
         | 
| 50 | 
            +
                  @queues[:jobs]    = MQ.new.queue("cukeq.jobs")
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def amqp_options
         | 
| 54 | 
            +
                  {
         | 
| 55 | 
            +
                    :host    => @host,
         | 
| 56 | 
            +
                    :port    => @port,
         | 
| 57 | 
            +
                    :user    => @user,
         | 
| 58 | 
            +
                    :pass    => @pass,
         | 
| 59 | 
            +
                    :vhost   => @vhost,
         | 
| 60 | 
            +
                    :timeout => 20
         | 
| 61 | 
            +
                  }
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              end # QueueHandler
         | 
| 65 | 
            +
            end # CukeQ
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            module EventMachine
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # http://github.com/eventmachine/eventmachine/issues#issue/15
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              def self.system3(cmd, *args, &cb)
         | 
| 8 | 
            +
                cb ||= args.pop if args.last.is_a? Proc
         | 
| 9 | 
            +
                init = args.pop if args.last.is_a? Proc
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                # merge remaining arguments into the command
         | 
| 12 | 
            +
                cmd = ([cmd] + args.map{|a|a.to_s.dump}).join(' ')
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                new_stderr = $stderr.dup
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                rd, wr = IO::pipe
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                result_count = 0
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                err_result = nil
         | 
| 21 | 
            +
                std_result = nil
         | 
| 22 | 
            +
                stderr_connection = nil
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                err_proc = proc {|output, status|
         | 
| 25 | 
            +
                  stderr_connection = nil
         | 
| 26 | 
            +
                  err_result = output
         | 
| 27 | 
            +
                  result_count+=1
         | 
| 28 | 
            +
                  if result_count == 2
         | 
| 29 | 
            +
                    cb[std_result, err_result, status]
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                }
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                std_proc = proc {|output, status|
         | 
| 34 | 
            +
                  stderr_connection.close_connection if stderr_connection
         | 
| 35 | 
            +
                  rd.close
         | 
| 36 | 
            +
                  std_result = output
         | 
| 37 | 
            +
                  result_count += 1
         | 
| 38 | 
            +
                  if result_count == 2
         | 
| 39 | 
            +
                    cb[std_result, err_result, status]
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                }
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                $stderr.reopen(wr)
         | 
| 44 | 
            +
                signature = EM.popen(cmd, SystemCmd, std_proc) do |c|
         | 
| 45 | 
            +
                  init[c] if init
         | 
| 46 | 
            +
                end.signature
         | 
| 47 | 
            +
                stderr_connection = EM.attach(rd, SystemCmd, err_proc)
         | 
| 48 | 
            +
                $stderr.reopen(new_stderr)
         | 
| 49 | 
            +
                wr.close
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                return EventMachine.get_subprocess_pid(signature)
         | 
| 52 | 
            +
              end unless EventMachine.respond_to?(:system3)
         | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module CukeQ
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # TODO: must be a better way
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              class JobClearer < Slave
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def poll
         | 
| 11 | 
            +
                  EM.next_tick {
         | 
| 12 | 
            +
                    job = @broker.queue_for(:jobs).pop
         | 
| 13 | 
            +
                    log(self.class, :ignoring, job)
         | 
| 14 | 
            +
                    poll
         | 
| 15 | 
            +
                  }
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              end # JobClearer
         | 
| 19 | 
            +
            end # CukeQ
         | 
    
        data/lib/cukeq/master.rb
    ADDED
    
    | @@ -0,0 +1,146 @@ | |
| 1 | 
            +
            module CukeQ
         | 
| 2 | 
            +
              class Master
         | 
| 3 | 
            +
                DEFAULT_BROKER_URI = URI.parse("amqp://cukeq-master:cukeq123@localhost:5672/cukeq")
         | 
| 4 | 
            +
                DEFAULT_WEBAPP_URI = URI.parse("http://0.0.0.0:9292")
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                  def execute(argv)
         | 
| 8 | 
            +
                    configured_instance(argv).start
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def configured_instance(argv)
         | 
| 12 | 
            +
                    opts = parse(argv)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    raise ArgumentError, "must provide --scm" unless opts[:scm]
         | 
| 15 | 
            +
                    raise ArgumentError, "must provide --report_to" unless opts[:report_to]
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    if b = opts[:broker]
         | 
| 18 | 
            +
                      b.user     ||= DEFAULT_BROKER_URI.user
         | 
| 19 | 
            +
                      b.password ||= DEFAULT_BROKER_URI.password
         | 
| 20 | 
            +
                      b.host     ||= DEFAULT_BROKER_URI.host
         | 
| 21 | 
            +
                      b.port     ||= DEFAULT_BROKER_URI.port
         | 
| 22 | 
            +
                      b.path     = b.path.empty? ? DEFAULT_BROKER_URI.path : b.path
         | 
| 23 | 
            +
                    else
         | 
| 24 | 
            +
                      opts[:broker] = DEFAULT_BROKER_URI
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    if w = opts[:webapp]
         | 
| 28 | 
            +
                      w.host ||= DEFAULT_WEBAPP_URI.host
         | 
| 29 | 
            +
                      w.port ||= DEFAULT_WEBAPP_URI.port
         | 
| 30 | 
            +
                    else
         | 
| 31 | 
            +
                      opts[:webapp] = DEFAULT_WEBAPP_URI
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    new(
         | 
| 35 | 
            +
                      Broker.new(opts[:broker]),
         | 
| 36 | 
            +
                      WebApp.new(opts[:webapp]),
         | 
| 37 | 
            +
                      Scm.new(opts[:scm]),
         | 
| 38 | 
            +
                      Reporter.new(opts[:report_to]),
         | 
| 39 | 
            +
                      ScenarioExploder.new
         | 
| 40 | 
            +
                    )
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def parse(argv)
         | 
| 44 | 
            +
                    options = {}
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    argv.extend OptionParser::Arguable
         | 
| 47 | 
            +
                    argv.options do |opts|
         | 
| 48 | 
            +
                      opts.on("-b", "--broker URI (default: #{DEFAULT_BROKER_URI})") do |str|
         | 
| 49 | 
            +
                        options[:broker] = URI.parse(str)
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                      opts.on("-w", "--webapp URI (default: http://localhost:9292)") do |str|
         | 
| 53 | 
            +
                        options[:webapp] = URI.parse(str)
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                      opts.on("-s", "--scm SCM-URL") do |url|
         | 
| 57 | 
            +
                        options[:scm] = URI.parse(url)
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      opts.on("-r", "--report-to REPORTER-URL") do |url|
         | 
| 61 | 
            +
                        options[:report_to] = URI.parse(url)
         | 
| 62 | 
            +
                      end
         | 
| 63 | 
            +
                    end.parse!
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    options
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end # class << self
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                attr_reader :broker, :webapp, :scm, :reporter, :exploder
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def initialize(broker, webapp, scm, reporter, exploder)
         | 
| 72 | 
            +
                  @broker   = broker
         | 
| 73 | 
            +
                  @webapp   = webapp
         | 
| 74 | 
            +
                  @scm      = scm
         | 
| 75 | 
            +
                  @reporter = reporter
         | 
| 76 | 
            +
                  @exploder = exploder
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def start
         | 
| 80 | 
            +
                  @scm.update {
         | 
| 81 | 
            +
                    @broker.start {
         | 
| 82 | 
            +
                      subscribe
         | 
| 83 | 
            +
                      start_webapp
         | 
| 84 | 
            +
                    }
         | 
| 85 | 
            +
                  }
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def ping(&blk)
         | 
| 89 | 
            +
                  log log_name, :ping
         | 
| 90 | 
            +
                  @broker.subscribe :pong, &blk
         | 
| 91 | 
            +
                  @broker.publish   :ping, '{}'
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                #
         | 
| 95 | 
            +
                # This is triggered by POSTs to the webapp
         | 
| 96 | 
            +
                #
         | 
| 97 | 
            +
                # data:
         | 
| 98 | 
            +
                #
         | 
| 99 | 
            +
                # { 'features' => (sent to exploder), 'run_id' => ID }
         | 
| 100 | 
            +
                #
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                def run(data)
         | 
| 103 | 
            +
                  Dir.chdir(@scm.working_copy) do
         | 
| 104 | 
            +
                    @exploder.explode(data['features']) { |units| publish_units(data, units) }
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                def publish_units(data, units)
         | 
| 109 | 
            +
                  scm = { :revision => @scm.current_revision, :url => @scm.url }
         | 
| 110 | 
            +
                  run = { :id => data['run_id'], :no_of_units => units.size}
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  units.each do |unit|
         | 
| 113 | 
            +
                    @broker.publish(
         | 
| 114 | 
            +
                      :jobs, {
         | 
| 115 | 
            +
                        :run         => run,
         | 
| 116 | 
            +
                        :scm         => scm,
         | 
| 117 | 
            +
                        :unit        => unit,
         | 
| 118 | 
            +
                      }.to_json
         | 
| 119 | 
            +
                    )
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                    log self.class, :published, unit
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                #
         | 
| 126 | 
            +
                # Called whenever a result is received on the results queue
         | 
| 127 | 
            +
                #
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def result(message)
         | 
| 130 | 
            +
                  log self.class, :result, message['run']
         | 
| 131 | 
            +
                  @reporter.report message
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def subscribe
         | 
| 135 | 
            +
                  @broker.subscribe :results do |message|
         | 
| 136 | 
            +
                    next unless message
         | 
| 137 | 
            +
                    result(JSON.parse(message))
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def start_webapp
         | 
| 142 | 
            +
                  @webapp.run method(:run)
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
              end # Master
         | 
| 146 | 
            +
            end # CukeQ
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module CukeQ
         | 
| 2 | 
            +
              class Reporter
         | 
| 3 | 
            +
                attr_reader :uri
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(uri)
         | 
| 6 | 
            +
                  @uri = uri
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def report(message)
         | 
| 10 | 
            +
                  EM::P::HttpClient.request(
         | 
| 11 | 
            +
                    :host    => uri.host,
         | 
| 12 | 
            +
                    :port    => uri.port,
         | 
| 13 | 
            +
                    :verb    => "POST",
         | 
| 14 | 
            +
                    :request => uri.path.empty? ? "/" : uri.path,
         | 
| 15 | 
            +
                    :content => message.to_json
         | 
| 16 | 
            +
                  )
         | 
| 17 | 
            +
                rescue => e # EM raises a RuntimeError..
         | 
| 18 | 
            +
                  log self.class, "error for #{uri}: #{e.message}"
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              end # Reporter
         | 
| 22 | 
            +
            end # CukeQ
         | 
    
        data/lib/cukeq/runner.rb
    ADDED
    
    | @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            module CukeQ
         | 
| 2 | 
            +
              class Runner
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                DEFAULT_TRIGGER_URI = URI.parse("http://localhost:9292/")
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def self.execute(args)
         | 
| 7 | 
            +
                  opts = parse(args)
         | 
| 8 | 
            +
                  uri = opts.delete(:uri)
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  EM.run {
         | 
| 11 | 
            +
                    http = EM::P::HttpClient.request(
         | 
| 12 | 
            +
                      :host    => uri.host,
         | 
| 13 | 
            +
                      :port    => uri.port,
         | 
| 14 | 
            +
                      :verb    => "POST",
         | 
| 15 | 
            +
                      :request => uri.path.empty? ? "/" : uri.path,
         | 
| 16 | 
            +
                      :content => opts.to_json
         | 
| 17 | 
            +
                    )
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    http.callback do |response|
         | 
| 20 | 
            +
                      log :success, response
         | 
| 21 | 
            +
                      EM.stop
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    http.errback do |error|
         | 
| 25 | 
            +
                      log :error, error[:status], uri.to_s
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      EM.stop
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  }
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                private
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def self.parse(argv)
         | 
| 36 | 
            +
                  options = {:uri => DEFAULT_TRIGGER_URI}
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  argv.extend OptionParser::Arguable
         | 
| 39 | 
            +
                  argv.options do |opts|
         | 
| 40 | 
            +
                    opts.on("-u", "--uri URI (default: #{DEFAULT_TRIGGER_URI})") do |str|
         | 
| 41 | 
            +
                      options[:uri] = URI.parse(str)
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    opts.on("-i", "--id RUN_ID") do |str|
         | 
| 45 | 
            +
                      options[:run_id] = str
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end.parse!
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  if argv.empty?
         | 
| 50 | 
            +
                    raise "must provide list of features"
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  unless options[:run_id]
         | 
| 54 | 
            +
                    raise "must provide --id"
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  options[:features] = argv
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  options
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              end # Runner
         | 
| 63 | 
            +
            end # CukeQ
         | 
| @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            module CukeQ
         | 
| 2 | 
            +
              class ScenarioExploder
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                def explode(file_colon_lines)
         | 
| 5 | 
            +
                  yield file_colon_lines.map { |f| {:file => f} } # temporary
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  # # cwd is now the working copy of the project
         | 
| 8 | 
            +
                  # units = []
         | 
| 9 | 
            +
                  #
         | 
| 10 | 
            +
                  # # we do the parsing in a subprocess to avoid having to restart the master
         | 
| 11 | 
            +
                  # # whenever gherkin/cucumber is updated.
         | 
| 12 | 
            +
                  # #
         | 
| 13 | 
            +
                  # # The slaves should do the same. CukeQ is just passing things through.
         | 
| 14 | 
            +
                  # #
         | 
| 15 | 
            +
                  # IO.popen("-") do |pipe|
         | 
| 16 | 
            +
                  #   if pipe
         | 
| 17 | 
            +
                  #     while json = pipe.gets
         | 
| 18 | 
            +
                  #       units << JSON.parse(json)
         | 
| 19 | 
            +
                  #     end
         | 
| 20 | 
            +
                  #   else
         | 
| 21 | 
            +
                  #     file_colon_lines.each do |f|
         | 
| 22 | 
            +
                  #       puts json_for(f)
         | 
| 23 | 
            +
                  #     end
         | 
| 24 | 
            +
                  #   end
         | 
| 25 | 
            +
                  # end
         | 
| 26 | 
            +
                  #
         | 
| 27 | 
            +
                  # yield units
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              end # ScenarioExploder
         | 
| 31 | 
            +
            end # CukeQ
         | 
| 32 | 
            +
             | 
| @@ -0,0 +1,134 @@ | |
| 1 | 
            +
            # encoding: utf-8
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "tmpdir"
         | 
| 4 | 
            +
            require "fileutils"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module CukeQ
         | 
| 7 | 
            +
              class ScenarioRunner
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def run(job, &callback)
         | 
| 10 | 
            +
                  scm = scm_for job
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  Dir.chdir scm.working_copy
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  run_job(scm.working_copy, job, callback)
         | 
| 15 | 
            +
                rescue => ex
         | 
| 16 | 
            +
                  yield :success   => false,
         | 
| 17 | 
            +
                        :error     => ex.message,
         | 
| 18 | 
            +
                        :backtrace => ex.backtrace,
         | 
| 19 | 
            +
                        :run       => job['run']
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def scm_for(job)
         | 
| 23 | 
            +
                  url = job['scm']['url']
         | 
| 24 | 
            +
                  rev = job['scm']['revision']
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  scm = Scm.new(url)
         | 
| 27 | 
            +
                  unless scm.current_revision.to_s == rev.to_s
         | 
| 28 | 
            +
                    # TODO(jari): this doesn't ensure that current_revision == rev - it
         | 
| 29 | 
            +
                    # would also make sense to move the logic to Scm
         | 
| 30 | 
            +
                    scm.update {} # hmm.
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  scm
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def run_job(working_copy, job, callback)
         | 
| 37 | 
            +
                  AsyncJob.new(working_copy, job, callback).run
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              end # ScenarioRunner
         | 
| 41 | 
            +
            end # CukeQ
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            class AsyncJob
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              def initialize(working_copy, job, callback)
         | 
| 46 | 
            +
                @job          = job
         | 
| 47 | 
            +
                @callback     = callback
         | 
| 48 | 
            +
                @result       = {:success => true, :slave => CukeQ.identifier}
         | 
| 49 | 
            +
                @invoked      = false
         | 
| 50 | 
            +
                @working_copy = working_copy
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
              def run
         | 
| 54 | 
            +
                parse_job
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                EventMachine.system3 command, &method(:child_finished)
         | 
| 57 | 
            +
              rescue => ex
         | 
| 58 | 
            +
                handle_exception(ex)
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
              private
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              def handle_exception(ex)
         | 
| 64 | 
            +
                @result.merge!(:success => false, :error => ex.message, :backtrace => ex.backtrace, :cwd => Dir.pwd)
         | 
| 65 | 
            +
                cleanup
         | 
| 66 | 
            +
                invoke_callback
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              def invoke_callback
         | 
| 70 | 
            +
                if @invoked
         | 
| 71 | 
            +
                  $stderr.puts "#{self} tried to invoke callback twice"
         | 
| 72 | 
            +
                  return
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                @invoked = true
         | 
| 76 | 
            +
                @callback.call @result
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              def cleanup
         | 
| 80 | 
            +
                FileUtils.rm_rf(output_file) if File.exist?(output_file)
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
              def command
         | 
| 84 | 
            +
                "cucumber -rfeatures --format Cucumber::Formatter::Json --out #{output_file} #{@feature_file}"
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
              def child_finished(stdout, stderr, status)
         | 
| 88 | 
            +
                output = <<-OUT
         | 
| 89 | 
            +
            stdout:
         | 
| 90 | 
            +
            #{stdout}
         | 
| 91 | 
            +
             | 
| 92 | 
            +
            stderr:
         | 
| 93 | 
            +
            #{stderr}
         | 
| 94 | 
            +
                OUT
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                @result.merge!(
         | 
| 97 | 
            +
                  :output   => output,
         | 
| 98 | 
            +
                  :stderr   => stderr,
         | 
| 99 | 
            +
                  :stdout   => stdout,
         | 
| 100 | 
            +
                  :success  => status.success?,
         | 
| 101 | 
            +
                  :exitcode => status.exitstatus,
         | 
| 102 | 
            +
                  :results  => fetch_results,
         | 
| 103 | 
            +
                  :cwd      => Dir.pwd
         | 
| 104 | 
            +
                )
         | 
| 105 | 
            +
                cleanup
         | 
| 106 | 
            +
                invoke_callback
         | 
| 107 | 
            +
              rescue => ex
         | 
| 108 | 
            +
                handle_exception ex
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
              def fetch_results
         | 
| 112 | 
            +
                return unless File.exist?(output_file)
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                content = File.read(output_file)
         | 
| 115 | 
            +
                begin
         | 
| 116 | 
            +
                  JSON.parse(content)
         | 
| 117 | 
            +
                rescue JSON::ParserError => ex
         | 
| 118 | 
            +
                  raise JSON::ParserError, "#{ex.message} (#{content.inspect})"
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
              end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
              def parse_job
         | 
| 123 | 
            +
                @feature_file = @job['unit']['file']
         | 
| 124 | 
            +
                @run          = @job['run']
         | 
| 125 | 
            +
                @scm          = @job['scm']
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                @result.merge!(:feature_file => @feature_file, :run => @run, :scm => @scm)
         | 
| 128 | 
            +
              end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
              def output_file
         | 
| 131 | 
            +
                @output_file ||= "#{CukeQ.identifier}-#{@run['id']}.json"
         | 
| 132 | 
            +
              end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
            end
         |