blade_runner 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dfcd2226f3c4c49763734dd9473ebb0477367713
4
+ data.tar.gz: 787fd069485edda1dd8948d97c66afbfb72aacf6
5
+ SHA512:
6
+ metadata.gz: 4c3609c082d83d0486f4578f70c5ecd6f8a2fc5ba84245d8a05545e9f1e71e8899f652ede77f2fd65c6d0a375ba322367fc7fc1364138b040ed1c9bf4cd49087
7
+ data.tar.gz: 5d14dcaa121b74e319ebabba9d1162ae367944163b1f5fb070b2afe08d9807d77edaa42048fbd685b2419314569753bc0ad6ae21875275df5ad133965d41d5f3
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in blade_runner.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Javan Makhmali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ Blade Runner: A [Sprockets](https://github.com/rails/sprockets)-friendly JavaScript test runner
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,38 @@
1
+ #= require faye-browser
2
+
3
+ websocketURL = document.querySelector("meta[name=websocket]").getAttribute("content")
4
+ @client = new Faye.Client websocketURL
5
+ session_id = document.cookie.match(/blade_runner_session=(\w+)/)?[1]
6
+
7
+ publish = ({channel, event, data}) ->
8
+ if session_id?
9
+ data = extend(copy(data), {event, session_id})
10
+ client.publish(channel, data)
11
+
12
+ copy = (object) ->
13
+ results = {}
14
+ results[key] = value for key, value of object
15
+ results
16
+
17
+ extend = (object, attributes) ->
18
+ object[key] = value for key, value of attributes
19
+ object
20
+
21
+ if session_id?
22
+ client.subscribe "/assets", (data) ->
23
+ location.reload() if data.changed
24
+
25
+ setInterval ->
26
+ publish(channel: "/browsers", event: "ping")
27
+ , 1000
28
+
29
+ @BladeRunner =
30
+ suiteBegin: ({total}) ->
31
+ publish("/tests", event: "begin", data: {total})
32
+
33
+ testResult: ({name, pass, message}) ->
34
+ result = pass
35
+ publish(channel: "/tests", event: "result", data: {result, name, message})
36
+
37
+ suiteEnd: (details) ->
38
+ publish(channel: "/tests", event: "end", data: details)
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Blade Runner</title>
6
+ <meta name="websocket" content="blade_runner/websocket">
7
+ <script src="blade_runner.js"></script>
8
+
9
+ <%= environment(:user)["head.html"] %>
10
+
11
+ <% if environment(:adapter)["index.css"] %>
12
+ <link rel="stylesheet" type="text/css" href="blade_runner/adapter.css">
13
+ <% end %>
14
+
15
+ <% if environment(:adapter)["index.js"] %>
16
+ <script src="blade_runner/adapter.js"
17
+ <% end %>
18
+
19
+ <%= environment(:adapter)["head.html"] %>
20
+
21
+ <% logical_paths(:js).each do |path| %>
22
+ <script src="<%= path %>"></script>
23
+ <% end %>
24
+ </head>
25
+ <body>
26
+ <%= environment(:user)["body.html"] %>
27
+ <%= environment(:adapter)["body.html"] %>
28
+ </body>
29
+ </html>
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "blade_runner"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'blade_runner/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "blade_runner"
8
+ spec.version = BladeRunner::VERSION
9
+ spec.authors = ["Javan Makhmali"]
10
+ spec.email = ["javan@javan.us"]
11
+
12
+ spec.summary = %q{Blade Runner}
13
+ spec.description = %q{A Sprockets-friendly JavaScript test runner}
14
+ spec.homepage = "https://github.com/javan/blade_runner"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+
25
+ spec.add_dependency "activesupport", ">= 3.0.0"
26
+ spec.add_dependency "coffee-script", "~> 2.4.0"
27
+ spec.add_dependency "coffee-script-source", "~> 1.9.0"
28
+ spec.add_dependency "curses", "~> 1.0.0"
29
+ spec.add_dependency "eventmachine", "~> 1.0.0"
30
+ spec.add_dependency "faye", "~> 1.1.1"
31
+ spec.add_dependency "sprockets", "~> 3.2.0"
32
+ spec.add_dependency "thin", "~> 1.6.0"
33
+ spec.add_dependency "useragent", "~> 0.13.0"
34
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "blade_runner"
4
+ BladeRunner.start
@@ -0,0 +1,166 @@
1
+ require "blade_runner/version"
2
+
3
+ require "eventmachine"
4
+ require "faye"
5
+ require "pathname"
6
+ require "ostruct"
7
+ require "yaml"
8
+
9
+ require "active_support/core_ext/hash"
10
+
11
+ module BladeRunner
12
+ extend self
13
+
14
+ @components = []
15
+
16
+ def register_component(component)
17
+ @components << component
18
+ end
19
+
20
+ require "blade_runner/component"
21
+ require "blade_runner/server"
22
+
23
+ autoload :Model, "blade_runner/model"
24
+ autoload :Assets, "blade_runner/assets"
25
+ autoload :RackAdapter, "blade_runner/rack_adapter"
26
+ autoload :Session, "blade_runner/session"
27
+ autoload :TestResults, "blade_runner/test_results"
28
+ autoload :CombinedTestResults, "blade_runner/combined_test_results"
29
+ autoload :Console, "blade_runner/interface/console"
30
+ autoload :CI, "blade_runner/interface/ci"
31
+
32
+ extend Forwardable
33
+ def_delegators "Server.client", :subscribe, :publish
34
+
35
+ ALLOWED_MODES = [:console, :ci]
36
+
37
+ DEFAULT_MODE = ALLOWED_MODES.first
38
+ DEFAULT_PORT = 9876
39
+
40
+ attr_reader :config, :plugins
41
+
42
+ @running = false
43
+ @initialized = false
44
+
45
+ def start(options = {})
46
+ return if running?
47
+ initialize!(options)
48
+ handle_exit
49
+ interface
50
+
51
+ EM.run do
52
+ @components.each { |c| c.start if c.respond_to?(:start) }
53
+ @running = true
54
+ end
55
+ end
56
+
57
+ def stop
58
+ return if @stopping
59
+ @stopping = true
60
+ @components.each { |c| c.stop if c.respond_to?(:stop) }
61
+ EM.stop if EM.reactor_running?
62
+ @running = false
63
+ end
64
+
65
+ def running?
66
+ @running
67
+ end
68
+
69
+ def initialize!(options = {})
70
+ return if initialized?
71
+ @options = options.with_indifferent_access
72
+ load_config_file!
73
+ read_arguments!
74
+ setup_config!
75
+ setup_adapter!
76
+ setup_plugins!
77
+ @initialized = true
78
+ end
79
+
80
+ def initialized?
81
+ @initialized
82
+ end
83
+
84
+ def url(path = "")
85
+ "http://localhost:#{config.port}#{path}"
86
+ end
87
+
88
+ def interface
89
+ @interface ||=
90
+ case config.mode
91
+ when :ci then CI
92
+ when :console then Console
93
+ end
94
+ end
95
+
96
+ def root_path
97
+ Pathname.new(File.dirname(__FILE__)).join("../")
98
+ end
99
+
100
+ def tmp_path
101
+ Pathname.new(".").join("tmp/blade_runner")
102
+ end
103
+
104
+ private
105
+ def handle_exit
106
+ %w( INT ).each do |signal|
107
+ trap(signal) { stop }
108
+ end
109
+
110
+ at_exit do
111
+ stop
112
+ exit $!.status if $!.is_a?(SystemExit)
113
+ end
114
+ end
115
+
116
+ def load_config_file!
117
+ filename = ".blade.yml"
118
+ if File.exists?(filename)
119
+ @options.reverse_merge!(YAML.load_file(filename))
120
+ end
121
+ end
122
+
123
+ def read_arguments!
124
+ if mode = ARGV[0]
125
+ @options[:mode] = mode.to_sym
126
+ end
127
+ end
128
+
129
+ def setup_config!
130
+ ignore_options = ALLOWED_MODES + [:plugins]
131
+
132
+ options = @options.except(ignore_options)
133
+ options[:mode] = DEFAULT_MODE unless ALLOWED_MODES.include?(options[:mode])
134
+
135
+ if options_for_mode = @options[options[:mode]]
136
+ options.merge! options_for_mode.except(ignore_options)
137
+ end
138
+
139
+ options[:port] ||= DEFAULT_PORT
140
+ options[:load_paths] = Array(options[:load_paths])
141
+ options[:logical_paths] = Array(options[:logical_paths])
142
+
143
+ @config = OpenStruct.new(options)
144
+ end
145
+
146
+ def setup_adapter!
147
+ require "blade_runner/#{config.framework}_adapter"
148
+ end
149
+
150
+ def setup_plugins!
151
+ @plugins = OpenStruct.new
152
+
153
+ plugin_config = @options[:plugins] || {}
154
+
155
+ if plugins_for_mode = (@options[config.mode] || {})[:plugins]
156
+ plugin_config.merge! plugins_for_mode
157
+ end
158
+
159
+ plugin_config.each do |name, plugin_config|
160
+ plugins[name] = OpenStruct.new(config: OpenStruct.new(plugin_config))
161
+ require "blade_runner/#{name}_plugin"
162
+ end
163
+ end
164
+ end
165
+
166
+ BR = BladeRunner
@@ -0,0 +1,73 @@
1
+ require "sprockets"
2
+
3
+ module BladeRunner::Assets
4
+ extend self
5
+
6
+ @environments = {}
7
+
8
+ def environment(name = :blade_runner)
9
+ @environments[name] ||= Sprockets::Environment.new do |env|
10
+ env.cache = Sprockets::Cache::FileStore.new(BR.tmp_path.join(name.to_s))
11
+
12
+ send("#{name}_load_paths").each do |path|
13
+ env.append_path(path)
14
+ end
15
+
16
+ env.context_class.class_eval do
17
+ extend Forwardable
18
+ def_delegators "BladeRunner::Assets", :environment, :logical_paths
19
+ end
20
+ end
21
+ end
22
+
23
+ def logical_paths(type = nil)
24
+ paths = BR.config.logical_paths
25
+ paths.select! { |path| File.extname(path) == ".#{type}" } if type
26
+ paths
27
+ end
28
+
29
+ def blade_runner_load_paths
30
+ [ BR.root_path.join("assets"), gem_pathname("faye").join("lib") ]
31
+ end
32
+
33
+ def user_load_paths
34
+ BR.config.load_paths.map { |a| Pathname.new(a) }
35
+ end
36
+
37
+ def adapter_load_paths
38
+ gem_name = "blade_runner-#{BR.config.framework}_adapter"
39
+ [ gem_pathname(gem_name).join("assets") ]
40
+ end
41
+
42
+ def watch_logical_paths
43
+ @mtimes = get_mtimes
44
+
45
+ EM.add_periodic_timer(1) do
46
+ mtimes = get_mtimes
47
+ unless mtimes == @mtimes
48
+ @mtimes = mtimes
49
+ BR.publish("/assets", changed: @mtimes)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+ def get_mtimes
56
+ {}.tap do |mtimes|
57
+ BR.config.logical_paths.each do |path|
58
+ mtimes[path] = get_mtime(path)
59
+ end
60
+ end
61
+ end
62
+
63
+ def get_mtime(logical_path)
64
+ environment_for(:user)[logical_path].mtime
65
+ rescue Exception => e
66
+ e.to_s
67
+ end
68
+
69
+ def gem_pathname(gem_name)
70
+ gemspec = Gem::Specification.find_by_name(gem_name)
71
+ Pathname.new(gemspec.gem_dir)
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ class BladeRunner::CombinedTestResults
2
+ attr_reader :sessions, :all_test_results
3
+
4
+ def initialize(sessions)
5
+ @sessions = sessions
6
+ @all_test_results = sessions.map(&:test_results)
7
+ end
8
+
9
+ def total
10
+ sum(totals)
11
+ end
12
+
13
+ def lines(type = :results)
14
+ sessions.flat_map do |session|
15
+ session.test_results.send(type).map do |line|
16
+ line.sub(/ok/, "ok [#{session}]")
17
+ end
18
+ end
19
+ end
20
+
21
+ def to_s
22
+ lines = ["1..#{total}"] + lines(:failures) + lines(:passes)
23
+ lines.join("\n")
24
+ end
25
+
26
+ def failed?
27
+ statuses.include?("failed")
28
+ end
29
+
30
+ private
31
+ def sum(values)
32
+ values.inject(0) { |sum, total| sum + total }
33
+ end
34
+
35
+ def totals
36
+ all_test_results.map(&:total).compact
37
+ end
38
+
39
+ def statuses
40
+ all_test_results.map(&:status)
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module BladeRunner::Component
2
+ def self.included(base)
3
+ BladeRunner.register_component(base)
4
+ end
5
+ end
@@ -0,0 +1,51 @@
1
+ module BladeRunner::CI
2
+ extend self
3
+ include BladeRunner::Component
4
+
5
+ def start
6
+ @completed_sessions = 0
7
+
8
+ log "# Running"
9
+ BR.subscribe("/results") do |details|
10
+ process_result(details)
11
+ end
12
+ end
13
+
14
+ private
15
+ def process_result(details)
16
+ if details.has_key?("pass")
17
+ log details["pass"] ? "." : "F"
18
+ end
19
+
20
+ if details["completed"]
21
+ process_completion
22
+ end
23
+ end
24
+
25
+ def process_completion
26
+ @completed_sessions += 1
27
+
28
+ if done?
29
+ log "\n"
30
+ display_results_and_exit
31
+ end
32
+ end
33
+
34
+ def done?
35
+ @completed_sessions == (BR.config.expected_sessions || 1)
36
+ end
37
+
38
+ def display_results_and_exit
39
+ results = BR::Session.combined_test_results
40
+ display results
41
+ exit results.failed? ? 1 : 0
42
+ end
43
+
44
+ def log(message)
45
+ STDERR.print message.to_s
46
+ end
47
+
48
+ def display(message)
49
+ STDOUT.puts message.to_s
50
+ end
51
+ end
@@ -0,0 +1,125 @@
1
+ require "curses"
2
+
3
+ module BladeRunner::Console
4
+ extend self
5
+ include BladeRunner::Component
6
+
7
+ autoload :Tab, "blade_runner/interface/console_tab"
8
+
9
+ extend Forwardable
10
+ def_delegators "BladeRunner::Console", :create_window
11
+
12
+ COLOR_NAMES = %w( white yellow green red )
13
+ PADDING = 1
14
+
15
+ def colors
16
+ @colors ||= OpenStruct.new.tap do |colors|
17
+ COLOR_NAMES.each do |name|
18
+ const = Curses.const_get("COLOR_#{name.upcase}")
19
+ Curses.init_pair(const, const, Curses::COLOR_BLACK)
20
+ colors[name] = Curses.color_pair(const)
21
+ end
22
+ end
23
+ end
24
+
25
+ def create_window(options = {})
26
+ height = options[:height] || 0
27
+ width = options[:width] || 0
28
+ top = options[:top] || 0
29
+ left = options[:left] || PADDING
30
+ parent = options[:parent] || Curses.stdscr
31
+
32
+ parent.subwin(height, width, top, left)
33
+ end
34
+
35
+ def start
36
+ run
37
+ BR::Assets.watch_logical_paths
38
+ end
39
+
40
+ def stop
41
+ Curses.close_screen
42
+ end
43
+
44
+ def run
45
+ start_screen
46
+ init_windows
47
+ handle_keys
48
+ handle_stale_tabs
49
+
50
+ BR.subscribe("/results") do |details|
51
+ session = BR::Session.find(details["session_id"])
52
+
53
+ if tab = Tab.find(session.id)
54
+ if details["line"] && tab.active?
55
+ Tab.content_window.addstr(details["line"] + "\n")
56
+ Tab.content_window.noutrefresh
57
+ end
58
+ tab.draw
59
+ else
60
+ tab = Tab.create(id: session.id)
61
+ tab.activate if Tab.size == 1
62
+ end
63
+
64
+ Curses.doupdate
65
+ end
66
+ end
67
+
68
+ private
69
+ def start_screen
70
+ Curses.init_screen
71
+ Curses.start_color
72
+ Curses.noecho
73
+ Curses.curs_set(0)
74
+ Curses.stdscr.keypad(true)
75
+ end
76
+
77
+ def init_windows
78
+ header_window = create_window(height: 3)
79
+ header_window.attron(Curses::A_BOLD)
80
+ header_window.addstr "BLADE RUNNER [press 'q' to quit]\n"
81
+ header_window.attroff(Curses::A_BOLD)
82
+ header_window.addstr "Open #{BR.url} to start"
83
+ header_window.noutrefresh
84
+
85
+ Tab.install(top: header_window.maxy)
86
+
87
+ Curses.doupdate
88
+ end
89
+
90
+ def handle_keys
91
+ EM.defer do
92
+ while ch = Curses.getch
93
+ case ch
94
+ when Curses::KEY_LEFT
95
+ Tab.active.activate_previous
96
+ Curses.doupdate
97
+ when Curses::KEY_RIGHT
98
+ Tab.active.activate_next
99
+ Curses.doupdate
100
+ when "q"
101
+ BR.stop
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def handle_stale_tabs
108
+ BR.subscribe("/browsers") do |details|
109
+ if details["message"] = "ping"
110
+ if tab = Tab.find(details["session_id"])
111
+ tab.last_ping_at = Time.now
112
+ end
113
+ end
114
+ end
115
+
116
+ EM.add_periodic_timer(1) do
117
+ Tab.stale.each { |t| remove_tab(t) }
118
+ end
119
+ end
120
+
121
+ def remove_tab(tab)
122
+ Tab.remove(tab.id)
123
+ Curses.doupdate
124
+ end
125
+ end
@@ -0,0 +1,173 @@
1
+ class BladeRunner::Console::Tab < BladeRunner::Model
2
+ extend Forwardable
3
+ def_delegators "BladeRunner::Console", :colors, :create_window
4
+
5
+ class << self
6
+ extend Forwardable
7
+ def_delegators "BladeRunner::Console", :create_window
8
+
9
+ attr_reader :window, :status_window, :content_window
10
+
11
+ def install(options = {})
12
+ top = options[:top]
13
+ @window = create_window(top: top, height: 3)
14
+
15
+ top = @window.begy + @window.maxy + 1
16
+ @status_window = create_window(top: top, height: 1)
17
+
18
+ top = @status_window.begy + @status_window.maxy + 1
19
+ @content_window = create_window(top: top)
20
+ @content_window.scrollok(true)
21
+ end
22
+
23
+ def draw
24
+ window.clear
25
+ window.noutrefresh
26
+ all.each(&:draw)
27
+ end
28
+
29
+ def remove(id)
30
+ tab = find(id)
31
+ tab.deactivate
32
+ tab.window.close
33
+ super
34
+ draw
35
+ end
36
+
37
+ def active
38
+ all.detect(&:active?)
39
+ end
40
+
41
+ def stale
42
+ threshold = Time.now - 2
43
+ all.select { |t| t.last_ping_at && t.last_ping_at < threshold }
44
+ end
45
+ end
46
+
47
+ def tabs
48
+ self.class
49
+ end
50
+
51
+ def height
52
+ 3
53
+ end
54
+
55
+ def width
56
+ 5
57
+ end
58
+
59
+ def top
60
+ tabs.window.begy
61
+ end
62
+
63
+ def left
64
+ tabs.window.begx + index * width
65
+ end
66
+
67
+ def window
68
+ @window ||= create_window(height: height, width: width, top: top, left: left)
69
+ end
70
+
71
+ def draw
72
+ window.clear
73
+ active? ? draw_active : draw_inactive
74
+ window.noutrefresh
75
+ end
76
+
77
+ def draw_active
78
+ window.addstr "╔═══╗"
79
+ window.addstr "║ "
80
+ window.attron(color)
81
+ window.addstr(dot)
82
+ window.attroff(color)
83
+ window.addstr(" ║")
84
+ window.addstr "╝ ╚"
85
+ end
86
+
87
+ def draw_inactive
88
+ window.addstr "\n"
89
+ window.attron(color)
90
+ window.addstr(" #{dot}\n")
91
+ window.attroff(color)
92
+ window.addstr "═════"
93
+ end
94
+
95
+ def dot
96
+ status == "pending" ? "○" : "●"
97
+ end
98
+
99
+ def index
100
+ tabs.all.index(self)
101
+ end
102
+
103
+ def session
104
+ BR::Session.find(id)
105
+ end
106
+
107
+ def status
108
+ session.test_results.status
109
+ end
110
+
111
+ def active?
112
+ active
113
+ end
114
+
115
+ def activate
116
+ return if active?
117
+
118
+ if tab = tabs.active
119
+ tab.deactivate
120
+ end
121
+
122
+ self.active = true
123
+ draw
124
+
125
+ tabs.status_window.addstr(session.to_s)
126
+ tabs.status_window.noutrefresh
127
+
128
+ tabs.content_window.addstr(session.test_results.to_s)
129
+ tabs.content_window.noutrefresh
130
+ end
131
+
132
+ def deactivate
133
+ return unless active?
134
+
135
+ self.active = false
136
+ draw
137
+
138
+ tabs.status_window.clear
139
+ tabs.status_window.noutrefresh
140
+
141
+ tabs.content_window.clear
142
+ tabs.content_window.noutrefresh
143
+ end
144
+
145
+ def activate_next
146
+ tabs = tabs.all
147
+
148
+ if tabs.last == self
149
+ tabs.first.activate
150
+ elsif tab = tabs[index + 1]
151
+ tab.activate
152
+ end
153
+ end
154
+
155
+ def activate_previous
156
+ tabs = tabs.all
157
+
158
+ if tabs.first == self
159
+ tabs.last.activate
160
+ elsif tab = tabs[index - 1]
161
+ tab.activate
162
+ end
163
+ end
164
+
165
+ def color
166
+ case status
167
+ when "running" then colors.yellow
168
+ when "finished" then colors.green
169
+ when /fail/ then colors.red
170
+ else colors.white
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,31 @@
1
+ require "securerandom"
2
+
3
+ class BladeRunner::Model < OpenStruct
4
+ class << self
5
+ def models
6
+ @models ||= {}
7
+ end
8
+
9
+ def create(attributes)
10
+ attributes[:id] ||= SecureRandom.hex(4)
11
+ model = new(attributes)
12
+ models[model.id] = model
13
+ end
14
+
15
+ def find(id)
16
+ models[id]
17
+ end
18
+
19
+ def remove(id)
20
+ models.delete(id)
21
+ end
22
+
23
+ def all
24
+ models.values
25
+ end
26
+
27
+ def size
28
+ models.size
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,81 @@
1
+ class BladeRunner::RackAdapter
2
+ extend Forwardable
3
+ def_delegators "BladeRunner::Assets", :environment
4
+
5
+ PATH = "/blade_runner"
6
+ ADAPTER_PATH = PATH + "/adapter"
7
+ WEBSOCKET_PATH = PATH + "/websocket"
8
+
9
+ def initialize(app = nil, options = {})
10
+ BR.initialize!
11
+
12
+ if app.is_a?(Hash)
13
+ @options = app
14
+ else
15
+ @app, @options = app, options
16
+ end
17
+
18
+ @mount_path = @options[:mount]
19
+ @mount_path_pattern = /^#{@mount_path}/
20
+
21
+ @blade_runner_path_pattern = /^#{PATH}/
22
+ @adapter_path_pattern = /^#{ADAPTER_PATH}/
23
+ @websocket_path_pattern = /^#{WEBSOCKET_PATH}/
24
+ end
25
+
26
+ def call(env)
27
+ unless @mount_path.nil?
28
+ if env["PATH_INFO"] =~ @mount_path_pattern
29
+ env["PATH_INFO"].sub!(@mount_path_pattern, "")
30
+ elsif @app
31
+ return @app.call(env)
32
+ end
33
+ end
34
+
35
+ case env["PATH_INFO"]
36
+ when ""
37
+ add_forward_slash(env)
38
+ when "/"
39
+ env["PATH_INFO"] = "index.html"
40
+ response = environment(:blade_runner).call(env)
41
+ response_with_session(response, env)
42
+ when @websocket_path_pattern
43
+ bayeux.call(env)
44
+ when @adapter_path_pattern
45
+ env["PATH_INFO"].sub!(@adapter_path_pattern, "")
46
+ environment(:adapter).call(env)
47
+ when @blade_runner_path_pattern
48
+ env["PATH_INFO"].sub!(@blade_runner_path_pattern, "")
49
+ environment(:blade_runner).call(env)
50
+ else
51
+ environment(:user).call(env)
52
+ end
53
+ end
54
+
55
+ private
56
+ def bayeux
57
+ @bayeux ||= Faye::RackAdapter.new(mount: WEBSOCKET_PATH, timeout: 25)
58
+ end
59
+
60
+ def add_forward_slash(env)
61
+ path = @mount_path || env["REQUEST_PATH"] || env["SCRIPT_NAME"]
62
+ redirect_to(File.join(path.to_s, "/"))
63
+ end
64
+
65
+ def redirect_to(location, status = 301)
66
+ [status, { Location: location }, []]
67
+ end
68
+
69
+ def response_with_session(response, env)
70
+ if BR.running?
71
+ user_agent = UserAgent.parse(env["HTTP_USER_AGENT"])
72
+ session = BR::Session.create(user_agent: user_agent)
73
+ status, headers, body = response
74
+ response = Rack::Response.new(body, status, headers)
75
+ response.set_cookie("blade_runner_session", session.id)
76
+ response.finish
77
+ else
78
+ response
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,29 @@
1
+ require "faye/websocket"
2
+ require "useragent"
3
+
4
+ module BladeRunner::Server
5
+ extend self
6
+ include BladeRunner::Component
7
+
8
+ WEBSOCKET_PATH = "/blade_runner/websocket"
9
+
10
+ def start
11
+ Faye::WebSocket.load_adapter("thin")
12
+ Rack::Server.start(app: app, Port: BR.config.port, server: "thin")
13
+ end
14
+
15
+ def websocket_url(path = "")
16
+ BR.url(WEBSOCKET_PATH + path)
17
+ end
18
+
19
+ def client
20
+ @client ||= Faye::Client.new(websocket_url)
21
+ end
22
+
23
+ private
24
+ def app
25
+ Rack::Builder.app do
26
+ run BladeRunner::RackAdapter.new
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ class BladeRunner::Session < BladeRunner::Model
2
+ class << self
3
+ def create(attributes)
4
+ model = super
5
+ model.test_results = BladeRunner::TestResults.new(model.id)
6
+ model
7
+ end
8
+
9
+ def combined_test_results
10
+ BladeRunner::CombinedTestResults.new(all)
11
+ end
12
+ end
13
+
14
+ def to_s
15
+ @to_s ||= "#{ua.browser} #{ua.version} #{ua.platform}"
16
+ end
17
+
18
+ private
19
+ def ua
20
+ user_agent
21
+ end
22
+ end
@@ -0,0 +1,91 @@
1
+ class BladeRunner::TestResults
2
+ attr_reader :session_id, :status, :lines, :passes, :failures
3
+
4
+ def initialize(session_id)
5
+ @session_id = session_id
6
+ reset
7
+
8
+ BR.subscribe("/tests") do |details|
9
+ if details["session_id"] == session_id
10
+ process_test_result(details)
11
+ end
12
+ end
13
+ end
14
+
15
+ def reset
16
+ @lines = []
17
+ @passes = []
18
+ @failures = []
19
+ @completed = false
20
+ @status = "pending"
21
+ @total = nil
22
+ end
23
+
24
+ def process_test_result(details)
25
+ publication = {}
26
+
27
+ case details["event"]
28
+ when "begin"
29
+ reset
30
+ @status = "running"
31
+ @total = details["total"]
32
+ @lines << publication[:line] = "1..#{@total}"
33
+ when "result"
34
+ pass = details["result"]
35
+ args = details.values_at("name", "message")
36
+
37
+ if pass
38
+ line = Pass.new(*args).to_s
39
+ @passes << line
40
+ else
41
+ line = Failure.new(*args).to_s
42
+ @failures << line
43
+ @status = "failing"
44
+ end
45
+ @lines << line
46
+
47
+ publication.merge!(line: line, pass: pass)
48
+ when "end"
49
+ @status = failures.any? ? "failed" : "finished"
50
+ @completed = true
51
+ end
52
+
53
+ publication.merge!(status: status, session_id: session_id, completed: @completed)
54
+ BR.publish("/results", publication)
55
+ end
56
+
57
+ def total
58
+ if @total
59
+ @total
60
+ elsif @completed
61
+ passes.size + failures.size
62
+ end
63
+ end
64
+
65
+ def to_s
66
+ lines.join("\n") + "\n"
67
+ end
68
+
69
+ class Pass
70
+ def initialize(name, message)
71
+ @name = name
72
+ @message = message
73
+ end
74
+
75
+ def to_s(number = nil)
76
+ ["ok", number, @name, message].compact.join(" ")
77
+ end
78
+
79
+ def message
80
+ unless @message.nil?
81
+ "\n" + @message.gsub(/^/, "# ").chomp
82
+ end
83
+ end
84
+ end
85
+
86
+ class Failure < Pass
87
+ def to_s(*args)
88
+ "not #{super}"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module BladeRunner
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,223 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blade_runner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Javan Makhmali
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: coffee-script
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 2.4.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 2.4.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: coffee-script-source
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.9.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.9.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: curses
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.0.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.0.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: eventmachine
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.0.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.0.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: faye
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.1.1
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.1.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: sprockets
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 3.2.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 3.2.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: thin
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 1.6.0
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 1.6.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: useragent
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 0.13.0
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.13.0
167
+ description: A Sprockets-friendly JavaScript test runner
168
+ email:
169
+ - javan@javan.us
170
+ executables:
171
+ - blade
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".gitignore"
176
+ - Gemfile
177
+ - LICENSE.txt
178
+ - README.md
179
+ - Rakefile
180
+ - assets/index.coffee
181
+ - assets/index.html.erb
182
+ - bin/console
183
+ - bin/setup
184
+ - blade_runner.gemspec
185
+ - exe/blade
186
+ - lib/blade_runner.rb
187
+ - lib/blade_runner/assets.rb
188
+ - lib/blade_runner/combined_test_results.rb
189
+ - lib/blade_runner/component.rb
190
+ - lib/blade_runner/interface/ci.rb
191
+ - lib/blade_runner/interface/console.rb
192
+ - lib/blade_runner/interface/console_tab.rb
193
+ - lib/blade_runner/model.rb
194
+ - lib/blade_runner/rack_adapter.rb
195
+ - lib/blade_runner/server.rb
196
+ - lib/blade_runner/session.rb
197
+ - lib/blade_runner/test_results.rb
198
+ - lib/blade_runner/version.rb
199
+ homepage: https://github.com/javan/blade_runner
200
+ licenses:
201
+ - MIT
202
+ metadata: {}
203
+ post_install_message:
204
+ rdoc_options: []
205
+ require_paths:
206
+ - lib
207
+ required_ruby_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ required_rubygems_version: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - ">="
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ requirements: []
218
+ rubyforge_project:
219
+ rubygems_version: 2.4.6
220
+ signing_key:
221
+ specification_version: 4
222
+ summary: Blade Runner
223
+ test_files: []