blade 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1 -0
- data/Rakefile +12 -0
- data/assets/index.coffee +39 -0
- data/assets/index.html.erb +34 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/blade.gemspec +38 -0
- data/exe/blade +10 -0
- data/lib/blade.rb +144 -0
- data/lib/blade/assets.rb +84 -0
- data/lib/blade/cli.rb +13 -0
- data/lib/blade/combined_test_results.rb +42 -0
- data/lib/blade/component.rb +5 -0
- data/lib/blade/interface/ci.rb +51 -0
- data/lib/blade/interface/console.rb +125 -0
- data/lib/blade/interface/console/tab.rb +173 -0
- data/lib/blade/model.rb +31 -0
- data/lib/blade/rack_adapter.rb +81 -0
- data/lib/blade/server.rb +29 -0
- data/lib/blade/session.rb +22 -0
- data/lib/blade/test_helper.rb +7 -0
- data/lib/blade/test_results.rb +91 -0
- data/lib/blade/version.rb +3 -0
- metadata +253 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
class Blade::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,51 @@
|
|
1
|
+
module Blade::CI
|
2
|
+
extend self
|
3
|
+
include Blade::Component
|
4
|
+
|
5
|
+
def start
|
6
|
+
@completed_sessions = 0
|
7
|
+
|
8
|
+
log "# Running"
|
9
|
+
Blade.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 == (Blade.config.expected_sessions || 1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def display_results_and_exit
|
39
|
+
results = Blade::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 Blade::Console
|
4
|
+
extend self
|
5
|
+
include Blade::Component
|
6
|
+
|
7
|
+
autoload :Tab, "blade/interface/console/tab"
|
8
|
+
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators "Blade::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
|
+
Blade::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
|
+
Blade.subscribe("/results") do |details|
|
51
|
+
session = Blade::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 #{Blade.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
|
+
Blade.stop
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def handle_stale_tabs
|
108
|
+
Blade.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 Blade::Console::Tab < Blade::Model
|
2
|
+
extend Forwardable
|
3
|
+
def_delegators "Blade::Console", :colors, :create_window
|
4
|
+
|
5
|
+
class << self
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators "Blade::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
|
+
Blade::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
|
data/lib/blade/model.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
class Blade::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 Blade::RackAdapter
|
2
|
+
extend Forwardable
|
3
|
+
def_delegators "Blade::Assets", :environment
|
4
|
+
|
5
|
+
PATH = "/blade"
|
6
|
+
ADAPTER_PATH = PATH + "/adapter"
|
7
|
+
WEBSOCKET_PATH = PATH + "/websocket"
|
8
|
+
|
9
|
+
def initialize(app = nil, options = {})
|
10
|
+
Blade.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_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).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_path_pattern
|
48
|
+
env["PATH_INFO"].sub!(@blade_path_pattern, "")
|
49
|
+
environment(:blade).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 Blade.running?
|
71
|
+
user_agent = UserAgent.parse(env["HTTP_USER_AGENT"])
|
72
|
+
session = Blade::Session.create(user_agent: user_agent)
|
73
|
+
status, headers, body = response
|
74
|
+
response = Rack::Response.new(body, status, headers)
|
75
|
+
response.set_cookie("blade_session", session.id)
|
76
|
+
response.finish
|
77
|
+
else
|
78
|
+
response
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|