blade 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module Blade::Component
2
+ def self.included(base)
3
+ Blade.register_component(base)
4
+ end
5
+ 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
@@ -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