seaweed 0.1.0
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/bin/seaweed +55 -0
- data/lib/Spec.coffee +308 -0
- data/lib/required_files.rb +58 -0
- data/lib/spec.rb +42 -0
- data/server/public/jquery-1.5.js +8176 -0
- data/server/public/spec.css +9 -0
- data/server/server.rb +65 -0
- data/server/views/index.slim +11 -0
- metadata +157 -0
data/bin/seaweed
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env jruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler'
|
4
|
+
require 'celerity'
|
5
|
+
require 'watchr'
|
6
|
+
|
7
|
+
require File.expand_path('../../lib/required_files', __FILE__)
|
8
|
+
require File.expand_path('../../server/server', __FILE__)
|
9
|
+
require 'net/http'
|
10
|
+
|
11
|
+
PORT = 4567
|
12
|
+
|
13
|
+
app = Rack::Builder.app do
|
14
|
+
run Sinatra::Application
|
15
|
+
end
|
16
|
+
|
17
|
+
mode = case ARGV.first
|
18
|
+
when 's', 'server'
|
19
|
+
'server'
|
20
|
+
when 't', 'terminal'
|
21
|
+
'terminal'
|
22
|
+
when 'a', 'auto'
|
23
|
+
'auto'
|
24
|
+
else
|
25
|
+
'auto'
|
26
|
+
end
|
27
|
+
|
28
|
+
if mode == 'server'
|
29
|
+
Rack::Handler.default.run app, :Port => PORT
|
30
|
+
else
|
31
|
+
Thread.new do
|
32
|
+
Rack::Handler.default.run app, :Port => PORT
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
page = Net::HTTP.get URI.parse("http://localhost:#{PORT}/")
|
37
|
+
rescue Errno::ECONNREFUSED
|
38
|
+
sleep 1
|
39
|
+
retry
|
40
|
+
end
|
41
|
+
|
42
|
+
browser = Celerity::Browser.new
|
43
|
+
browser.goto "http://localhost:#{PORT}/#terminal"
|
44
|
+
puts browser.text
|
45
|
+
|
46
|
+
if mode == 'auto'
|
47
|
+
script = Watchr::Script.new
|
48
|
+
script.watch(/^(lib|spec)\/.*\.coffee$/) do
|
49
|
+
browser.refresh
|
50
|
+
puts browser.text
|
51
|
+
end
|
52
|
+
controller = Watchr::Controller.new(script, Watchr.handler.new)
|
53
|
+
controller.run
|
54
|
+
end
|
55
|
+
end
|
data/lib/Spec.coffee
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
# Seaweed Coffeescript spec framework
|
2
|
+
window.Spec = {
|
3
|
+
EnvironmentInitialized: false
|
4
|
+
|
5
|
+
# Adds indentation to a string
|
6
|
+
Pad: (string, times) ->
|
7
|
+
for i in [1..times]
|
8
|
+
string = ' ' + string
|
9
|
+
string
|
10
|
+
|
11
|
+
# Escapes text for HTML
|
12
|
+
Escape: (string) ->
|
13
|
+
$('<div/>').text(String(string)).html()
|
14
|
+
|
15
|
+
# Executes a test case
|
16
|
+
describe: (title, definition) ->
|
17
|
+
@initializeEnvironment() unless @EnvironmentInitialized
|
18
|
+
|
19
|
+
ul = $('<ul></ul>')
|
20
|
+
switch @Format
|
21
|
+
when 'ul'
|
22
|
+
$('.results').append($('<li>' + title + '</li>').append(ul))
|
23
|
+
when 'terminal'
|
24
|
+
$('.results').append "#{title}<br>"
|
25
|
+
ul.depth = 2
|
26
|
+
|
27
|
+
@testStack = [{
|
28
|
+
title: title
|
29
|
+
ul: ul
|
30
|
+
before: []
|
31
|
+
}]
|
32
|
+
|
33
|
+
definition()
|
34
|
+
|
35
|
+
# Displays a summary of error rate at the end of testing
|
36
|
+
finalize: ->
|
37
|
+
summary = "#{@counts.passed} passed, #{@counts.failed} failed, #{@counts.pending} pending, #{@counts.total} total"
|
38
|
+
switch @Format
|
39
|
+
when 'ul'
|
40
|
+
document.title = summary
|
41
|
+
if @errors.length
|
42
|
+
$('<h3>Errors</h3>').appendTo document.body
|
43
|
+
ul = $('<ul></ul>').addClass('errors').appendTo(document.body)
|
44
|
+
for error in @errors
|
45
|
+
ul.append $('<li>').append($('<span>').html(error.message), ' - ', $('<span>').html(error.title))
|
46
|
+
when 'terminal'
|
47
|
+
$('.results').append "<br>"
|
48
|
+
for error in @errors
|
49
|
+
$('.results').append "[31m#{error.message}[0m #{error.title}<br>"
|
50
|
+
color = if @counts.failed > 0
|
51
|
+
31
|
52
|
+
else if @counts.pending > 0
|
53
|
+
33
|
54
|
+
else
|
55
|
+
32
|
56
|
+
$('.results').append "[1m[#{color}m#{summary}[0m<br>"
|
57
|
+
|
58
|
+
# Extends the environment with test methods
|
59
|
+
initializeEnvironment: ->
|
60
|
+
@EnvironmentInitialized = true
|
61
|
+
|
62
|
+
@errors = []
|
63
|
+
@counts = {
|
64
|
+
passed: 0
|
65
|
+
failed: 0
|
66
|
+
pending: 0
|
67
|
+
total: 0
|
68
|
+
}
|
69
|
+
|
70
|
+
@Format = 'ul'
|
71
|
+
@Format = 'terminal' if location.hash == '#terminal'
|
72
|
+
|
73
|
+
# Add results display element to the page
|
74
|
+
switch @Format
|
75
|
+
when 'ul'
|
76
|
+
$('body').append('<ul class="results"></ul>')
|
77
|
+
when 'terminal'
|
78
|
+
$('body').append('<div class="results"></div>')
|
79
|
+
|
80
|
+
# Tests for a positive match
|
81
|
+
Object.prototype.should = (matcher) ->
|
82
|
+
result = matcher(this)
|
83
|
+
Spec.fail "expected #{result[1]}" unless result[0]
|
84
|
+
|
85
|
+
# Tests for a negative match
|
86
|
+
Object.prototype.shouldNot = (matcher) ->
|
87
|
+
result = matcher(this)
|
88
|
+
Spec.fail "expected not #{result[1]}" if result[0]
|
89
|
+
|
90
|
+
# Sets up an expectation
|
91
|
+
window.expectation = (message) ->
|
92
|
+
exp = {
|
93
|
+
message: message
|
94
|
+
meet: -> @met++
|
95
|
+
met: 0
|
96
|
+
desired: 1
|
97
|
+
twice: ->
|
98
|
+
@desired = 2
|
99
|
+
this
|
100
|
+
exactly: (times) ->
|
101
|
+
@desired = times
|
102
|
+
{times: this}
|
103
|
+
timesString: (times) ->
|
104
|
+
switch times
|
105
|
+
when 0
|
106
|
+
'not at all'
|
107
|
+
when 1
|
108
|
+
'once'
|
109
|
+
when 2
|
110
|
+
'twice'
|
111
|
+
else
|
112
|
+
"#{times} times"
|
113
|
+
check: ->
|
114
|
+
if @met != @desired
|
115
|
+
Spec.fail "expected #{message} #{@timesString @desired}, actually received #{@timesString @met}"
|
116
|
+
}
|
117
|
+
Spec.expectations.push exp
|
118
|
+
exp
|
119
|
+
|
120
|
+
# Creates a stub method with an expectation
|
121
|
+
Object.prototype.shouldReceive = (name) ->
|
122
|
+
object = this
|
123
|
+
|
124
|
+
received = expectation "to receive “#{name}”"
|
125
|
+
|
126
|
+
passthrough = object[name]
|
127
|
+
object[name] = -> received.meet()
|
128
|
+
|
129
|
+
received.with = (expectArgs...) ->
|
130
|
+
object[name] = (args...) ->
|
131
|
+
received.meet()
|
132
|
+
correct = true
|
133
|
+
correct = false if expectArgs.length != args.length
|
134
|
+
if correct
|
135
|
+
for i in [0..args.length]
|
136
|
+
correct = false unless String(expectArgs[i]) == String(args[i])
|
137
|
+
unless correct
|
138
|
+
Spec.fail "expected ##{name} to be called with arguments “#{expectArgs.join ', '}”, actual arguments: “#{args.join ', '}”"
|
139
|
+
received
|
140
|
+
|
141
|
+
received.andReturn = (returnValue) ->
|
142
|
+
fn = object[name]
|
143
|
+
object[name] = ->
|
144
|
+
fn.apply this, arguments
|
145
|
+
returnValue
|
146
|
+
received
|
147
|
+
|
148
|
+
received.andPassthrough = ->
|
149
|
+
fn = object[name]
|
150
|
+
object[name] = ->
|
151
|
+
fn.apply this, arguments
|
152
|
+
passthrough.apply this, arguments
|
153
|
+
received
|
154
|
+
|
155
|
+
received
|
156
|
+
|
157
|
+
# Creates a stub method, with an expectation of no calls
|
158
|
+
Object.prototype.shouldNotReceive = (name) ->
|
159
|
+
@shouldReceive(name).exactly(0).times
|
160
|
+
|
161
|
+
# Allows an assertion on a non-object value
|
162
|
+
window.expect = (object) ->
|
163
|
+
{
|
164
|
+
to: (matcher) ->
|
165
|
+
result = matcher(object)
|
166
|
+
Spec.fail "expected #{result[1]}" unless result[0]
|
167
|
+
notTo: (matcher) ->
|
168
|
+
result = matcher(object)
|
169
|
+
Spec.fail "expected not #{result[1]}" if result[0]
|
170
|
+
}
|
171
|
+
|
172
|
+
# Adds a setup step to the current test case
|
173
|
+
window.beforeEach = (action) ->
|
174
|
+
test = Spec.testStack[Spec.testStack.length - 1]
|
175
|
+
test.before.push action
|
176
|
+
|
177
|
+
# Prepares a sub-test of the current test case
|
178
|
+
window.describe = window.context = (title, definition) ->
|
179
|
+
parent = Spec.testStack[Spec.testStack.length - 1]
|
180
|
+
|
181
|
+
ul = $('<ul></ul>')
|
182
|
+
switch Spec.Format
|
183
|
+
when 'ul'
|
184
|
+
parent.ul.append($('<li>' + title + '</li>').append(ul))
|
185
|
+
when 'terminal'
|
186
|
+
$('.results').append(Spec.Pad(title, parent.ul.depth) + "<br>")
|
187
|
+
ul.depth = parent.ul.depth + 2
|
188
|
+
|
189
|
+
Spec.testStack.push {
|
190
|
+
title: title
|
191
|
+
ul: ul
|
192
|
+
before: []
|
193
|
+
}
|
194
|
+
definition()
|
195
|
+
Spec.testStack.pop()
|
196
|
+
|
197
|
+
# Creates a specificaition
|
198
|
+
window.it = (title, definition) ->
|
199
|
+
test = Spec.testStack[Spec.testStack.length - 1]
|
200
|
+
status = if definition?
|
201
|
+
env = {sandbox: $('<div/>').appendTo document.body}
|
202
|
+
for aTest in Spec.testStack
|
203
|
+
for action in aTest.before
|
204
|
+
action.call env
|
205
|
+
|
206
|
+
Spec.expectations = []
|
207
|
+
Spec.testTitle = title
|
208
|
+
|
209
|
+
window.onerror = (message) ->
|
210
|
+
Spec.fail "Error: #{message}"
|
211
|
+
|
212
|
+
Spec.passed = true
|
213
|
+
try
|
214
|
+
definition.call env
|
215
|
+
catch e
|
216
|
+
Spec.fail 'Error: ' + e
|
217
|
+
|
218
|
+
for expectation in Spec.expectations
|
219
|
+
expectation.check()
|
220
|
+
|
221
|
+
delete Spec.expectations
|
222
|
+
delete Spec.testTitle
|
223
|
+
delete window.onerror
|
224
|
+
|
225
|
+
env.sandbox.empty().remove()
|
226
|
+
|
227
|
+
if Spec.passed then "passed"; else "failed"
|
228
|
+
else
|
229
|
+
"pending"
|
230
|
+
|
231
|
+
switch Spec.Format
|
232
|
+
when 'ul'
|
233
|
+
li = $('<li>' + title + '</li>')
|
234
|
+
li.addClass status
|
235
|
+
|
236
|
+
test.ul.append li
|
237
|
+
when 'terminal'
|
238
|
+
s = title
|
239
|
+
color = switch status
|
240
|
+
when 'passed' then 32
|
241
|
+
when 'failed' then 31
|
242
|
+
when 'pending' then 33
|
243
|
+
$('.results').append Spec.Pad("[#{color}m#{s}[0m<br>", test.ul.depth)
|
244
|
+
|
245
|
+
Spec.counts[status]++
|
246
|
+
Spec.counts.total++
|
247
|
+
|
248
|
+
# Tests if matched value is a function
|
249
|
+
window.beAFunction = (value) ->
|
250
|
+
[typeof value is 'function', "to have type “function”, actual “#{typeof value}”"]
|
251
|
+
|
252
|
+
# Tests if matched value === expected value
|
253
|
+
window.be = (expected) ->
|
254
|
+
(value) ->
|
255
|
+
[value is expected, "to be “#{Spec.Escape expected}”, actual “#{Spec.Escape value}”"]
|
256
|
+
|
257
|
+
# Tests if matched value is boolean true
|
258
|
+
window.beTrue = (value) ->
|
259
|
+
[String(value) == 'true', "to be true, got “#{Spec.Escape value}”"]
|
260
|
+
|
261
|
+
# Tests if matched value is boolean false
|
262
|
+
window.beFalse = (value) ->
|
263
|
+
[String(value) == 'false', "to be false, got “#{Spec.Escape value}”"]
|
264
|
+
|
265
|
+
# Tests if matched value is an instance of class
|
266
|
+
window.beAnInstanceOf = (klass) ->
|
267
|
+
(value) ->
|
268
|
+
[value instanceof klass, "to be an instance of “#{klass}”"]
|
269
|
+
|
270
|
+
# Tests if matched value == expected value
|
271
|
+
window.equal = (expected) ->
|
272
|
+
(value) ->
|
273
|
+
[String(value) == String(expected), "to equal “#{Spec.Escape expected}”, actual “#{Spec.Escape value}”"]
|
274
|
+
|
275
|
+
# Fails test, with an error message
|
276
|
+
fail: (message) ->
|
277
|
+
@passed = false
|
278
|
+
@error = message
|
279
|
+
titles = []
|
280
|
+
for item in @testStack
|
281
|
+
titles.push item.title
|
282
|
+
titles.push @testTitle
|
283
|
+
@errors.push {
|
284
|
+
title: titles.join ' '
|
285
|
+
message: message
|
286
|
+
}
|
287
|
+
|
288
|
+
# Cleans test environment initialized with #initializeEnvironment
|
289
|
+
uninitializeEnvironment: ->
|
290
|
+
@EnvironmentInitialized = false
|
291
|
+
|
292
|
+
delete Object.prototype.should
|
293
|
+
delete Object.prototype.shouldNot
|
294
|
+
delete window.expectation
|
295
|
+
delete Object.prototype.shouldReceive
|
296
|
+
delete Object.prototype.shouldNotReceive
|
297
|
+
delete window.expect
|
298
|
+
delete window.beforeEach
|
299
|
+
delete window.describe
|
300
|
+
delete window.context
|
301
|
+
delete window.it
|
302
|
+
delete window.beAFunction
|
303
|
+
delete window.be
|
304
|
+
delete window.beTrue
|
305
|
+
delete window.beFalse
|
306
|
+
delete window.beAnInstanceOf
|
307
|
+
delete window.equal
|
308
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
class RequiredFiles < Hash
|
4
|
+
include TSort
|
5
|
+
|
6
|
+
attr_accessor :paths
|
7
|
+
|
8
|
+
# Adds a file to required list, and recursively scans and adds all other
|
9
|
+
# files required by this file
|
10
|
+
def add name
|
11
|
+
file_name = full_path name
|
12
|
+
if file_name && !self[name.to_sym]
|
13
|
+
requirements = find_requirements file_name
|
14
|
+
self[name.to_sym] = {
|
15
|
+
:file_name => file_name,
|
16
|
+
:requires => requirements
|
17
|
+
}
|
18
|
+
requirements.each { |requirement| add requirement }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Gets an array of file paths sorted in dependency order
|
23
|
+
def sorted_files
|
24
|
+
tsort.map{ |key| self[key][:file_name] }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
# Searches for file in path with given name and returns full file path,
|
29
|
+
# or nil if not found
|
30
|
+
def full_path name
|
31
|
+
file_path = @paths.find { |path| File.exists? "#{path}/#{name}.coffee" }
|
32
|
+
raise "File not found: #{name}" unless file_path
|
33
|
+
file_path && "#{file_path}/#{name}.coffee"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Scans a coffeescript file for requirement directives.
|
37
|
+
#
|
38
|
+
# Example requirement directives:
|
39
|
+
# #require ST
|
40
|
+
# #require ST/Model/Index
|
41
|
+
def find_requirements name
|
42
|
+
requirements = []
|
43
|
+
File.open name do |file|
|
44
|
+
file.each do |line|
|
45
|
+
if line.match /^#require\s+(\S+)\s*$/
|
46
|
+
requirements << $1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
requirements
|
51
|
+
end
|
52
|
+
|
53
|
+
# Callback functions for tsort
|
54
|
+
alias tsort_each_node each_key
|
55
|
+
def tsort_each_child node
|
56
|
+
self[node][:requires].each{ |name| yield name.to_sym }
|
57
|
+
end
|
58
|
+
end
|
data/lib/spec.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env jruby
|
2
|
+
require "rubygems"
|
3
|
+
require "bundler/setup"
|
4
|
+
require "coffee-script"
|
5
|
+
require "celerity"
|
6
|
+
|
7
|
+
Dir.mkdir 'compiled' unless File.exists? 'compiled'
|
8
|
+
raise "Can't create compile directory" unless File.directory? 'compiled'
|
9
|
+
|
10
|
+
# Compile coffee scripts
|
11
|
+
files = Dir['lib/**/*.coffee', 'spec/**/*.coffee']
|
12
|
+
longest_name = files.map(&:length).max
|
13
|
+
for file in files
|
14
|
+
begin
|
15
|
+
mtime = File.mtime file
|
16
|
+
|
17
|
+
outfile = file.sub /^(lib|spec)/, 'compiled'
|
18
|
+
outfile = outfile.sub /\.coffee$/, '.js'
|
19
|
+
|
20
|
+
next if File.exists?(outfile) && File.mtime(outfile) == mtime
|
21
|
+
|
22
|
+
dir = outfile.sub /\/[^\/]+$/, ''
|
23
|
+
Dir.mkdir dir unless File.exists? dir
|
24
|
+
File.open outfile, 'w' do |f|
|
25
|
+
f.write CoffeeScript.compile(File.read(file))
|
26
|
+
end
|
27
|
+
File.utime mtime, mtime, outfile
|
28
|
+
puts outfile
|
29
|
+
rescue CoffeeScript::CompilationError => e
|
30
|
+
if e.message.match /^SyntaxError: (.*) on line (\d+)\D*$/
|
31
|
+
puts "\e[31m#{file}:#{$2}".ljust(longest_name + 6) + " #{$1}\e[0m"
|
32
|
+
elsif e.message.match /^Parse error on line (\d+): (.*)$/
|
33
|
+
puts "\e[31m#{file}:#{$1}".ljust(longest_name + 6) + " #{$2}\e[0m"
|
34
|
+
else
|
35
|
+
puts "#{file} - #{e.message}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
browser = Celerity::Browser.new
|
41
|
+
browser.goto 'http://localhost/~tobico/SeaTurtle/spec/spec.html#terminal'
|
42
|
+
puts browser.text
|