lab_bench 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +20 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +30 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/assets/css/app.css +71 -0
- data/assets/images/spinner.gif +0 -0
- data/assets/index.html +18 -0
- data/assets/js/app.js +88 -0
- data/assets/js/head.min.js +7 -0
- data/assets/js/mustache.js +325 -0
- data/bin/lab_bench_server +52 -0
- data/lib/lab_bench/test_runner.rb +57 -0
- data/lib/lab_bench.rb +6 -0
- data/test/helper.rb +18 -0
- data/test/test_lab_bench.rb +7 -0
- metadata +226 -0
data/.document
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ree-1.8.7-2010.02@lab_bench
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.5.1"
|
12
|
+
gem "rcov", ">= 0"
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
git (1.2.5)
|
5
|
+
jeweler (1.5.1)
|
6
|
+
bundler (~> 1.0.0)
|
7
|
+
git (>= 1.2.5)
|
8
|
+
rake
|
9
|
+
rake (0.8.7)
|
10
|
+
rcov (0.9.9)
|
11
|
+
shoulda (2.11.3)
|
12
|
+
|
13
|
+
PLATFORMS
|
14
|
+
ruby
|
15
|
+
|
16
|
+
DEPENDENCIES
|
17
|
+
bundler (~> 1.0.0)
|
18
|
+
jeweler (~> 1.5.1)
|
19
|
+
rcov
|
20
|
+
shoulda
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Bradley Buda
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
= lab_bench
|
2
|
+
|
3
|
+
A browser-based runner for ruby Test::Unit tests.
|
4
|
+
|
5
|
+
== TODO
|
6
|
+
|
7
|
+
* Show me test failures as they happen
|
8
|
+
* Give me more context on the test suite (names of files being run, command line, project directory, etc)
|
9
|
+
* Allow me to rerun a suite
|
10
|
+
* Allow me to rerun a test
|
11
|
+
* Allow me to rerun all failed tests within a suite
|
12
|
+
* Continuous rerunning of failed tests (like autotest)
|
13
|
+
* Rerunning of failed tests when files change (like autotests)
|
14
|
+
* Notifications for success and failure
|
15
|
+
|
16
|
+
== Contributing to lab_bench
|
17
|
+
|
18
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
19
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
20
|
+
* Fork the project
|
21
|
+
* Start a feature/bugfix branch
|
22
|
+
* Commit and push until you are happy with your contribution
|
23
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
24
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
25
|
+
|
26
|
+
== Copyright
|
27
|
+
|
28
|
+
Copyright (c) 2010 Bradley Buda. See LICENSE.txt for
|
29
|
+
further details.
|
30
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "lab_bench"
|
16
|
+
gem.homepage = "http://github.com/bradleybuda/lab_bench"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{A browser-based runner for Test::Unit}
|
19
|
+
gem.description = %Q{Like autotest, but in your browser. Currently unsuitable for use by anyone.}
|
20
|
+
gem.email = "bradleybuda@gmail.com"
|
21
|
+
gem.authors = ["Bradley Buda"]
|
22
|
+
|
23
|
+
gem.add_runtime_dependency 'sinatra', '~> 1.1.0'
|
24
|
+
gem.add_runtime_dependency 'eventmachine', '~> 0.12.10'
|
25
|
+
gem.add_runtime_dependency 'em-websocket', '~> 0.2.0'
|
26
|
+
gem.add_runtime_dependency 'thin', '~> 1.2.0'
|
27
|
+
gem.add_runtime_dependency 'yajl-ruby', '~> 0.7.8'
|
28
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
29
|
+
|
30
|
+
gem.files.include 'assets/**/*'
|
31
|
+
end
|
32
|
+
Jeweler::RubygemsDotOrgTasks.new
|
33
|
+
|
34
|
+
require 'rake/testtask'
|
35
|
+
Rake::TestTask.new(:test) do |test|
|
36
|
+
test.libs << 'lib' << 'test'
|
37
|
+
test.pattern = 'test/**/test_*.rb'
|
38
|
+
test.verbose = true
|
39
|
+
end
|
40
|
+
|
41
|
+
require 'rcov/rcovtask'
|
42
|
+
Rcov::RcovTask.new do |test|
|
43
|
+
test.libs << 'test'
|
44
|
+
test.pattern = 'test/**/test_*.rb'
|
45
|
+
test.verbose = true
|
46
|
+
end
|
47
|
+
|
48
|
+
task :default => :test
|
49
|
+
|
50
|
+
require 'rake/rdoctask'
|
51
|
+
Rake::RDocTask.new do |rdoc|
|
52
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
53
|
+
|
54
|
+
rdoc.rdoc_dir = 'rdoc'
|
55
|
+
rdoc.title = "lab_bench #{version}"
|
56
|
+
rdoc.rdoc_files.include('README*')
|
57
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/assets/css/app.css
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
testruns {
|
2
|
+
display: block;
|
3
|
+
overflow-y: scroll;
|
4
|
+
height: 65%;
|
5
|
+
position: fixed;
|
6
|
+
top: 0;
|
7
|
+
left: 0;
|
8
|
+
right: 0;
|
9
|
+
}
|
10
|
+
|
11
|
+
testrun {
|
12
|
+
display: block;
|
13
|
+
}
|
14
|
+
|
15
|
+
testrunsummary {
|
16
|
+
display: block;
|
17
|
+
margin: 3px;
|
18
|
+
border: 1px solid #aaa;
|
19
|
+
left: 0;
|
20
|
+
right: 0;
|
21
|
+
padding: 3px;
|
22
|
+
padding-left: 25px;
|
23
|
+
}
|
24
|
+
|
25
|
+
testrunsummary started, testrunsummary currenttest, testrunsummary statitics {
|
26
|
+
display: block;
|
27
|
+
}
|
28
|
+
|
29
|
+
/* TODO icons for these */
|
30
|
+
testrunsummary.Running {
|
31
|
+
background-image: url(/images/spinner.gif);
|
32
|
+
background-repeat: no-repeat;
|
33
|
+
background-position: 3px 4px;
|
34
|
+
}
|
35
|
+
|
36
|
+
testrunsummary.Success {
|
37
|
+
background-color: lightgreen;
|
38
|
+
}
|
39
|
+
|
40
|
+
testrunsummary.Failed {
|
41
|
+
background-color: #FFBBBB;
|
42
|
+
}
|
43
|
+
|
44
|
+
eventlog {
|
45
|
+
display: block;
|
46
|
+
position: fixed;
|
47
|
+
bottom: 0;
|
48
|
+
left: 0;
|
49
|
+
right: 0;
|
50
|
+
height: 35%;
|
51
|
+
border-top: 3px solid black;
|
52
|
+
overflow-y: scroll;
|
53
|
+
}
|
54
|
+
|
55
|
+
testevent {
|
56
|
+
display: block;
|
57
|
+
border-bottom: 1px solid black;
|
58
|
+
line-height: 1.3em;
|
59
|
+
padding-top: 2px;
|
60
|
+
padding-bottom: 2px;
|
61
|
+
}
|
62
|
+
|
63
|
+
testevent timestamp {
|
64
|
+
display: inline-block;
|
65
|
+
padding-left: 5px;
|
66
|
+
padding-right: 5px;
|
67
|
+
}
|
68
|
+
|
69
|
+
testevent.testFault {
|
70
|
+
background-color: #FFBBBB;
|
71
|
+
}
|
Binary file
|
data/assets/index.html
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
<!DOCTYPE HTML>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>LabBench Test Status</title>
|
5
|
+
<link rel="stylesheet" href="/css/app.css">
|
6
|
+
<script src="/js/head.min.js"></script>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<testruns></testruns>
|
10
|
+
<eventlog></eventlog>
|
11
|
+
|
12
|
+
<script type='text/javascript'>
|
13
|
+
head.js("http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js");
|
14
|
+
head.js("/js/mustache.js");
|
15
|
+
head.js("/js/app.js");
|
16
|
+
</script>
|
17
|
+
</body>
|
18
|
+
</html>
|
data/assets/js/app.js
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
head.ready(function(){
|
2
|
+
var socket = new WebSocket('ws://localhost:9021/');
|
3
|
+
|
4
|
+
var suites = {};
|
5
|
+
|
6
|
+
var templates = {
|
7
|
+
"Test::Unit::UI::TestRunnerMediator::STARTED":
|
8
|
+
"<testevent class='suiteStarted'><timestamp>{{timestamp}}</timestamp>Test Suite Started</testevent>",
|
9
|
+
|
10
|
+
"Test::Unit::TestCase::STARTED":
|
11
|
+
"<testevent class='testStarted'><timestamp>{{timestamp}}</timestamp>Test Started: {{args}}</testevent>",
|
12
|
+
|
13
|
+
"FAULT":
|
14
|
+
"<testevent class='testFault'><timestamp>{{timestamp}}</timestamp>{{args}}</testevent>",
|
15
|
+
|
16
|
+
"Test::Unit::TestCase::FINISHED":
|
17
|
+
"<testevent class='testFinished'><timestamp>{{timestamp}}</timestamp>Test Completed: {{args}}</testevent>",
|
18
|
+
|
19
|
+
"Test::Unit::UI::TestRunnerMediator::FINISHED":
|
20
|
+
"<testevent class='suiteFinished'><timestamp>{{timestamp}}</timestamp>Test Suite Completed in {{args}} seconds</testevent>"
|
21
|
+
};
|
22
|
+
|
23
|
+
socket.onopen = function (e) {
|
24
|
+
// should indicate that we're connected
|
25
|
+
};
|
26
|
+
|
27
|
+
socket.onclose = function (e) {
|
28
|
+
// should indicate that we're disconnected and maybe try to reconnect
|
29
|
+
};
|
30
|
+
|
31
|
+
socket.onmessage = function(jsonMessage) {
|
32
|
+
if (jsonMessage && jsonMessage.data) {
|
33
|
+
var message = $.parseJSON(jsonMessage.data);
|
34
|
+
|
35
|
+
// append message to event log
|
36
|
+
message.timestamp = new Date(message.milliseconds).toLocaleTimeString();
|
37
|
+
var template = templates[message.event];
|
38
|
+
$('eventlog')
|
39
|
+
.append(Mustache.to_html(template, message))
|
40
|
+
.scrollTop($('eventlog').attr('scrollHeight') - $('eventlog').height());
|
41
|
+
|
42
|
+
// find or create the corresponding suite
|
43
|
+
var guid = message.guid;
|
44
|
+
if ($('#' + guid).length == 0) {
|
45
|
+
$('testruns').prepend('<testrun id=' + guid + '>');
|
46
|
+
}
|
47
|
+
|
48
|
+
// update its data based on the event
|
49
|
+
var testrun = $('#' + guid);
|
50
|
+
var testrunData = testrun.data();
|
51
|
+
if (message.event === "Test::Unit::UI::TestRunnerMediator::STARTED") {
|
52
|
+
testrunData.guid = guid;
|
53
|
+
testrunData.timestamp = new Date(message.milliseconds).toLocaleTimeString();
|
54
|
+
testrunData.status = 'Initializing';
|
55
|
+
testrunData.testCount = 0;
|
56
|
+
testrunData.failedCount = 0;
|
57
|
+
} else if (message.event === "Test::Unit::TestCase::STARTED") {
|
58
|
+
testrunData.status = 'Running';
|
59
|
+
testrunData.currentTest = message.args;
|
60
|
+
testrunData.running = true;
|
61
|
+
} else if (message.event === "FAULT") {
|
62
|
+
testrunData.failedCount += 1;
|
63
|
+
} else if (message.event === "Test::Unit::TestCase::FINISHED") {
|
64
|
+
testrunData.running = false;
|
65
|
+
testrunData.testCount += 1;
|
66
|
+
} else if (message.event === "Test::Unit::UI::TestRunnerMediator::FINISHED") {
|
67
|
+
if (testrunData.failedCount == 0) {
|
68
|
+
testrunData.status = 'Success';
|
69
|
+
} else {
|
70
|
+
testrunData.status = 'Failed';
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
// rerender the suite based on the updated data
|
75
|
+
testrun.html(Mustache.to_html("\
|
76
|
+
<testrunsummary class='{{status}}'> \
|
77
|
+
<started>Test Run Started {{timestamp}} - {{status}}</started> \
|
78
|
+
{{#running}} \
|
79
|
+
<currenttest>Current Test: {{currentTest}}</currenttest> \
|
80
|
+
{{/running}} \
|
81
|
+
<statistics> \
|
82
|
+
<testcount>{{testCount}} total test(s)</testcount> \
|
83
|
+
<failedcount>{{failedCount}} failure(s)</failedcount> \
|
84
|
+
</statistics> \
|
85
|
+
</testrunsummary>", testrunData));
|
86
|
+
}
|
87
|
+
};
|
88
|
+
});
|
@@ -0,0 +1,7 @@
|
|
1
|
+
/**
|
2
|
+
Head JS: The only script in your <HEAD>
|
3
|
+
|
4
|
+
copyright: "tipiirai" / Tero Piirainen
|
5
|
+
license: MIT
|
6
|
+
*/
|
7
|
+
(function(a){var b=a.documentElement,c={screens:[320,480,640,768,1024,1280,1440,1680,1920],section:"-section",page:"-page",head:"head"},d=[];if(typeof window.head_conf=="object")for(var e in head_conf)head_conf[e]&&(c[e]=head_conf[e]);window.head_conf=c;function f(a){d.push(a)}function g(a){var c=new RegExp("\\b"+a+"\\b");b.className=b.className.replace(c,"")}function h(a,b){for(var c=0;c<a.length;c++)b.call(a,a[c],c)}var i=window[c.head]=function(){i.ready.apply(null,arguments)};i.feature=function(a,c,e){a?e||(g("no-"+a),g(a)):(b.className+=" "+d.join(" "),d=[]),f((c?"":"no-")+a),i[a]=c;return i};var j=navigator.userAgent.toLowerCase();j=/(webkit)[ \/]([\w.]+)/.exec(j)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(j)||/(msie) ([\w.]+)/.exec(j)||!/compatible/.test(j)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(j)||[],j[1]=="msie"&&(j[1]="ie"),f(j[1]),i.browser={version:j[2]},i.browser[j[1]]=!0;if(i.browser.ie)for(var k=3;k<11;k++)parseFloat(j[2])<k&&f("lt-ie"+k);h("abbr|article|aside|audio|canvas|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video".split("|"),function(b){a.createElement(b)});var l=location.pathname,m=l.split("/"),n=m.slice(0,m.length-1).join("-")||"root",o=m.slice(-1)[0]||"index",p=o.indexOf(".");p>=0&&(o=o.substring(0,p)),i.section=n,i.pageId=o,f(n+c.section),b.id=o+c.page;function q(){var a=document.width||window.outerWidth||document.documentElement.clientWidth;b.className=b.className.replace(/ (w|lt)-\d+/g,""),f("w-"+Math.round(a/100)*100),h(c.screens,function(b){a<=b&&f("lt-"+b)})}q(),window.onresize=q,i.feature("script",!0).feature()})(document),function(a){var b=document.createElement("i"),c=b.style,d=" -o- -moz- -ms- -webkit- -khtml- ".split(" ");function e(a){c.cssText=d.join(a+";");var b=c.cssText?c.cssText.length:0;return b>0&&!c.cssText.split(";")[1]}var f={gradient:function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(#fff));",e="linear-gradient(left top,#eee,#fff);";c.cssText=(a+d.join(b+a)+d.join(e+a)).slice(0,-a.length);return c.backgroundImage},rgba:function(){c.cssText="background-color:rgba(0,0,0,0.5)";return c.backgroundColor},boxshadow:function(){return e("box-shadow: 0 0 0 red")},textshadow:function(){return c.textShadow===""},multiplebgs:function(){c.cssText="background:url(//:),url(//:),red url(//:)";return(new RegExp("(url\\s*\\(.*?){3}")).test(c.background)},borderimage:function(){return e("border-image: url(m.png) 1 1 stretch")},borderradius:function(){return e("border-radius:0")},opacity:function(){return e("opacity:.1")},reflections:function(){return e("box-reflect:right 0")},transforms:function(){return e("transform:rotate(1deg)")},transitions:function(){return e("transition:all .1s linear")}};for(var g in f)f[g]&&a.feature(g,f[g].call(),!0);a.feature()}(window[head_conf.head]),function(a){var b=a.documentElement,c=!1,d=[],e=[],f={},g={},h=window.head_conf&&head_conf.head||"head",i=window[h]=window[h]||{};i.js=function(){var a=arguments,b=[].slice.call(a,1),e=b[0];if(!c){d.push(function(){i.js.apply(null,a)});return i}e?(l(e)||m.apply(null,b),p(j(a[0]),l(e)?e:function(){i.js.apply(null,b)})):p(j(a[0]));return i},i.ready=function(a,b){if(l(a))return e.push(a);var c=f[a];c?c.push(b):c=f[a]=[b];return i};function j(a){var b=g[a.url||a];if(b)return b;if(typeof a=="object")for(var c in a)a[c]&&(b={name:c,url:a[c]});else b={name:a.substring(a.indexOf("/",10)+1,a.indexOf("?")),url:a};g[b.url]=b;return b}function k(a,b){if(a){typeof a=="object"&&(a=[].slice.call(a));for(var c=0;c<a.length;c++)b.call(a,a[c],c)}}function l(a){return Object.prototype.toString.call(a)=="[object Function]"}function m(){k(arguments,function(a){l(a)||o(j(a))})}function n(a){a.state="preloaded",k(a.onpreload,function(a){a.call()})}function o(c,d){if(!c.state){c.state="preloading",c.onpreload=[];if(/Firefox/.test(navigator.userAgent)){var e=a.createElement("object");e.data=c.url,e.width=0,e.height=0,e.onload=function(){n(c),setTimeout(function(){b.removeChild(e)},1)},b.appendChild(e)}else q({src:c.url,type:"cache"},function(){n(c)})}}function p(a,b){if(a.state=="loaded")return b();if(a.state=="preloading")return a.onpreload.push(function(){p(a,b)});a.state="loading",q(a.url,function(){a.state="loaded",b&&b.call(),k(f[a.name],function(a){a.call()});var c=!0;for(var d in g)g[d].state!="loaded"&&(c=!1);c&&k(e,function(a){a.done||a.call(),a.done=!0})})}function q(c,d){var e=a.createElement("script");e.type="text/"+(c.type||"javascript"),e.src=c.src||c,e.onreadystatechange=e.onload=function(){d.done||(d.call(),d.done=!0),document.all||b.removeChild(e)},b.appendChild(e)}setTimeout(function(){c=!0,k(d,function(a){a.call()})},200)}(document)
|
@@ -0,0 +1,325 @@
|
|
1
|
+
/*
|
2
|
+
mustache.js — Logic-less templates in JavaScript
|
3
|
+
|
4
|
+
See http://mustache.github.com/ for more info.
|
5
|
+
*/
|
6
|
+
|
7
|
+
var Mustache = function() {
|
8
|
+
var Renderer = function() {};
|
9
|
+
|
10
|
+
Renderer.prototype = {
|
11
|
+
otag: "{{",
|
12
|
+
ctag: "}}",
|
13
|
+
pragmas: {},
|
14
|
+
buffer: [],
|
15
|
+
pragmas_implemented: {
|
16
|
+
"IMPLICIT-ITERATOR": true
|
17
|
+
},
|
18
|
+
context: {},
|
19
|
+
|
20
|
+
render: function(template, context, partials, in_recursion) {
|
21
|
+
// reset buffer & set context
|
22
|
+
if(!in_recursion) {
|
23
|
+
this.context = context;
|
24
|
+
this.buffer = []; // TODO: make this non-lazy
|
25
|
+
}
|
26
|
+
|
27
|
+
// fail fast
|
28
|
+
if(!this.includes("", template)) {
|
29
|
+
if(in_recursion) {
|
30
|
+
return template;
|
31
|
+
} else {
|
32
|
+
this.send(template);
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
template = this.render_pragmas(template);
|
38
|
+
var html = this.render_section(template, context, partials);
|
39
|
+
if(in_recursion) {
|
40
|
+
return this.render_tags(html, context, partials, in_recursion);
|
41
|
+
}
|
42
|
+
|
43
|
+
this.render_tags(html, context, partials, in_recursion);
|
44
|
+
},
|
45
|
+
|
46
|
+
/*
|
47
|
+
Sends parsed lines
|
48
|
+
*/
|
49
|
+
send: function(line) {
|
50
|
+
if(line != "") {
|
51
|
+
this.buffer.push(line);
|
52
|
+
}
|
53
|
+
},
|
54
|
+
|
55
|
+
/*
|
56
|
+
Looks for %PRAGMAS
|
57
|
+
*/
|
58
|
+
render_pragmas: function(template) {
|
59
|
+
// no pragmas
|
60
|
+
if(!this.includes("%", template)) {
|
61
|
+
return template;
|
62
|
+
}
|
63
|
+
|
64
|
+
var that = this;
|
65
|
+
var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
|
66
|
+
this.ctag);
|
67
|
+
return template.replace(regex, function(match, pragma, options) {
|
68
|
+
if(!that.pragmas_implemented[pragma]) {
|
69
|
+
throw({message:
|
70
|
+
"This implementation of mustache doesn't understand the '" +
|
71
|
+
pragma + "' pragma"});
|
72
|
+
}
|
73
|
+
that.pragmas[pragma] = {};
|
74
|
+
if(options) {
|
75
|
+
var opts = options.split("=");
|
76
|
+
that.pragmas[pragma][opts[0]] = opts[1];
|
77
|
+
}
|
78
|
+
return "";
|
79
|
+
// ignore unknown pragmas silently
|
80
|
+
});
|
81
|
+
},
|
82
|
+
|
83
|
+
/*
|
84
|
+
Tries to find a partial in the curent scope and render it
|
85
|
+
*/
|
86
|
+
render_partial: function(name, context, partials) {
|
87
|
+
name = this.trim(name);
|
88
|
+
if(!partials || partials[name] === undefined) {
|
89
|
+
throw({message: "unknown_partial '" + name + "'"});
|
90
|
+
}
|
91
|
+
if(typeof(context[name]) != "object") {
|
92
|
+
return this.render(partials[name], context, partials, true);
|
93
|
+
}
|
94
|
+
return this.render(partials[name], context[name], partials, true);
|
95
|
+
},
|
96
|
+
|
97
|
+
/*
|
98
|
+
Renders inverted (^) and normal (#) sections
|
99
|
+
*/
|
100
|
+
render_section: function(template, context, partials) {
|
101
|
+
if(!this.includes("#", template) && !this.includes("^", template)) {
|
102
|
+
return template;
|
103
|
+
}
|
104
|
+
|
105
|
+
var that = this;
|
106
|
+
// CSW - Added "+?" so it finds the tighest bound, not the widest
|
107
|
+
var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
|
108
|
+
"\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
|
109
|
+
"\\s*", "mg");
|
110
|
+
|
111
|
+
// for each {{#foo}}{{/foo}} section do...
|
112
|
+
return template.replace(regex, function(match, type, name, content) {
|
113
|
+
var value = that.find(name, context);
|
114
|
+
if(type == "^") { // inverted section
|
115
|
+
if(!value || that.is_array(value) && value.length === 0) {
|
116
|
+
// false or empty list, render it
|
117
|
+
return that.render(content, context, partials, true);
|
118
|
+
} else {
|
119
|
+
return "";
|
120
|
+
}
|
121
|
+
} else if(type == "#") { // normal section
|
122
|
+
if(that.is_array(value)) { // Enumerable, Let's loop!
|
123
|
+
return that.map(value, function(row) {
|
124
|
+
return that.render(content, that.create_context(row),
|
125
|
+
partials, true);
|
126
|
+
}).join("");
|
127
|
+
} else if(that.is_object(value)) { // Object, Use it as subcontext!
|
128
|
+
return that.render(content, that.create_context(value),
|
129
|
+
partials, true);
|
130
|
+
} else if(typeof value === "function") {
|
131
|
+
// higher order section
|
132
|
+
return value.call(context, content, function(text) {
|
133
|
+
return that.render(text, context, partials, true);
|
134
|
+
});
|
135
|
+
} else if(value) { // boolean section
|
136
|
+
return that.render(content, context, partials, true);
|
137
|
+
} else {
|
138
|
+
return "";
|
139
|
+
}
|
140
|
+
}
|
141
|
+
});
|
142
|
+
},
|
143
|
+
|
144
|
+
/*
|
145
|
+
Replace {{foo}} and friends with values from our view
|
146
|
+
*/
|
147
|
+
render_tags: function(template, context, partials, in_recursion) {
|
148
|
+
// tit for tat
|
149
|
+
var that = this;
|
150
|
+
|
151
|
+
var new_regex = function() {
|
152
|
+
return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
|
153
|
+
that.ctag + "+", "g");
|
154
|
+
};
|
155
|
+
|
156
|
+
var regex = new_regex();
|
157
|
+
var tag_replace_callback = function(match, operator, name) {
|
158
|
+
switch(operator) {
|
159
|
+
case "!": // ignore comments
|
160
|
+
return "";
|
161
|
+
case "=": // set new delimiters, rebuild the replace regexp
|
162
|
+
that.set_delimiters(name);
|
163
|
+
regex = new_regex();
|
164
|
+
return "";
|
165
|
+
case ">": // render partial
|
166
|
+
return that.render_partial(name, context, partials);
|
167
|
+
case "{": // the triple mustache is unescaped
|
168
|
+
return that.find(name, context);
|
169
|
+
default: // escape the value
|
170
|
+
return that.escape(that.find(name, context));
|
171
|
+
}
|
172
|
+
};
|
173
|
+
var lines = template.split("\n");
|
174
|
+
for(var i = 0; i < lines.length; i++) {
|
175
|
+
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
|
176
|
+
if(!in_recursion) {
|
177
|
+
this.send(lines[i]);
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
if(in_recursion) {
|
182
|
+
return lines.join("\n");
|
183
|
+
}
|
184
|
+
},
|
185
|
+
|
186
|
+
set_delimiters: function(delimiters) {
|
187
|
+
var dels = delimiters.split(" ");
|
188
|
+
this.otag = this.escape_regex(dels[0]);
|
189
|
+
this.ctag = this.escape_regex(dels[1]);
|
190
|
+
},
|
191
|
+
|
192
|
+
escape_regex: function(text) {
|
193
|
+
// thank you Simon Willison
|
194
|
+
if(!arguments.callee.sRE) {
|
195
|
+
var specials = [
|
196
|
+
'/', '.', '*', '+', '?', '|',
|
197
|
+
'(', ')', '[', ']', '{', '}', '\\'
|
198
|
+
];
|
199
|
+
arguments.callee.sRE = new RegExp(
|
200
|
+
'(\\' + specials.join('|\\') + ')', 'g'
|
201
|
+
);
|
202
|
+
}
|
203
|
+
return text.replace(arguments.callee.sRE, '\\$1');
|
204
|
+
},
|
205
|
+
|
206
|
+
/*
|
207
|
+
find `name` in current `context`. That is find me a value
|
208
|
+
from the view object
|
209
|
+
*/
|
210
|
+
find: function(name, context) {
|
211
|
+
name = this.trim(name);
|
212
|
+
|
213
|
+
// Checks whether a value is thruthy or false or 0
|
214
|
+
function is_kinda_truthy(bool) {
|
215
|
+
return bool === false || bool === 0 || bool;
|
216
|
+
}
|
217
|
+
|
218
|
+
var value;
|
219
|
+
if(is_kinda_truthy(context[name])) {
|
220
|
+
value = context[name];
|
221
|
+
} else if(is_kinda_truthy(this.context[name])) {
|
222
|
+
value = this.context[name];
|
223
|
+
}
|
224
|
+
|
225
|
+
if(typeof value === "function") {
|
226
|
+
return value.apply(context);
|
227
|
+
}
|
228
|
+
if(value !== undefined) {
|
229
|
+
return value;
|
230
|
+
}
|
231
|
+
// silently ignore unkown variables
|
232
|
+
return "";
|
233
|
+
},
|
234
|
+
|
235
|
+
// Utility methods
|
236
|
+
|
237
|
+
/* includes tag */
|
238
|
+
includes: function(needle, haystack) {
|
239
|
+
return haystack.indexOf(this.otag + needle) != -1;
|
240
|
+
},
|
241
|
+
|
242
|
+
/*
|
243
|
+
Does away with nasty characters
|
244
|
+
*/
|
245
|
+
escape: function(s) {
|
246
|
+
s = String(s === null ? "" : s);
|
247
|
+
return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
|
248
|
+
switch(s) {
|
249
|
+
case "&": return "&";
|
250
|
+
case "\\": return "\\\\";
|
251
|
+
case '"': return '"';
|
252
|
+
case "'": return ''';
|
253
|
+
case "<": return "<";
|
254
|
+
case ">": return ">";
|
255
|
+
default: return s;
|
256
|
+
}
|
257
|
+
});
|
258
|
+
},
|
259
|
+
|
260
|
+
// by @langalex, support for arrays of strings
|
261
|
+
create_context: function(_context) {
|
262
|
+
if(this.is_object(_context)) {
|
263
|
+
return _context;
|
264
|
+
} else {
|
265
|
+
var iterator = ".";
|
266
|
+
if(this.pragmas["IMPLICIT-ITERATOR"]) {
|
267
|
+
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
|
268
|
+
}
|
269
|
+
var ctx = {};
|
270
|
+
ctx[iterator] = _context;
|
271
|
+
return ctx;
|
272
|
+
}
|
273
|
+
},
|
274
|
+
|
275
|
+
is_object: function(a) {
|
276
|
+
return a && typeof a == "object";
|
277
|
+
},
|
278
|
+
|
279
|
+
is_array: function(a) {
|
280
|
+
return Object.prototype.toString.call(a) === '[object Array]';
|
281
|
+
},
|
282
|
+
|
283
|
+
/*
|
284
|
+
Gets rid of leading and trailing whitespace
|
285
|
+
*/
|
286
|
+
trim: function(s) {
|
287
|
+
return s.replace(/^\s*|\s*$/g, "");
|
288
|
+
},
|
289
|
+
|
290
|
+
/*
|
291
|
+
Why, why, why? Because IE. Cry, cry cry.
|
292
|
+
*/
|
293
|
+
map: function(array, fn) {
|
294
|
+
if (typeof array.map == "function") {
|
295
|
+
return array.map(fn);
|
296
|
+
} else {
|
297
|
+
var r = [];
|
298
|
+
var l = array.length;
|
299
|
+
for(var i = 0; i < l; i++) {
|
300
|
+
r.push(fn(array[i]));
|
301
|
+
}
|
302
|
+
return r;
|
303
|
+
}
|
304
|
+
}
|
305
|
+
};
|
306
|
+
|
307
|
+
return({
|
308
|
+
name: "mustache.js",
|
309
|
+
version: "0.3.1-dev",
|
310
|
+
|
311
|
+
/*
|
312
|
+
Turns a template and view into HTML
|
313
|
+
*/
|
314
|
+
to_html: function(template, view, partials, send_fun) {
|
315
|
+
var renderer = new Renderer();
|
316
|
+
if(send_fun) {
|
317
|
+
renderer.send = send_fun;
|
318
|
+
}
|
319
|
+
renderer.render(template, view, partials);
|
320
|
+
if(!send_fun) {
|
321
|
+
return renderer.buffer.join("\n");
|
322
|
+
}
|
323
|
+
}
|
324
|
+
});
|
325
|
+
}();
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'lab_bench'
|
5
|
+
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'em-websocket'
|
8
|
+
require 'sinatra/base'
|
9
|
+
require 'thin'
|
10
|
+
require 'yajl'
|
11
|
+
|
12
|
+
SOCKETS = []
|
13
|
+
|
14
|
+
class LabBenchServer < Sinatra::Base
|
15
|
+
set :public, LabBench::ASSETS
|
16
|
+
|
17
|
+
get '/' do
|
18
|
+
redirect '/index.html'
|
19
|
+
end
|
20
|
+
|
21
|
+
# lets the babyship ensure that we're up and running
|
22
|
+
get '/ping' do
|
23
|
+
'Pong!'
|
24
|
+
end
|
25
|
+
|
26
|
+
# give the babyship a chance to post events, that we just rebroadcast
|
27
|
+
post '/test_event' do
|
28
|
+
message = Yajl::Encoder.encode(params.merge(:milliseconds => (Time.now.to_f * 1000).to_i))
|
29
|
+
SOCKETS.each { |s| s.send message }
|
30
|
+
|
31
|
+
# should vend back a UID for this test run or something
|
32
|
+
# might also need to accept a UID that the client has already given us
|
33
|
+
'Okay'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
EventMachine.run do
|
38
|
+
# HACK - websockets and HTTP on different (adjacent) ports
|
39
|
+
EventMachine::WebSocket.start(:host => '0.0.0.0', :port => 9021) do |ws|
|
40
|
+
ws.onopen do
|
41
|
+
SOCKETS << ws
|
42
|
+
end
|
43
|
+
|
44
|
+
ws.onclose do
|
45
|
+
SOCKETS.delete(ws)
|
46
|
+
end
|
47
|
+
|
48
|
+
ws.onerror { |e| puts "Error: #{e.message}" }
|
49
|
+
end
|
50
|
+
|
51
|
+
LabBenchServer.run!(:host => '0.0.0.0', :port => 9020)
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test/unit/ui/console/testrunner'
|
3
|
+
require 'test/unit/ui/testrunnermediator'
|
4
|
+
|
5
|
+
# might be a little hacky to subclass this, but it already provides so many useful methods
|
6
|
+
class LabBench::TestRunner < Test::Unit::UI::Console::TestRunner
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super(*args)
|
10
|
+
|
11
|
+
connect_to_mothership
|
12
|
+
@guid = "suite_#{rand(2**64).to_s}"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def attach_to_mediator
|
18
|
+
# Add listeners for the five main testing events and relay then to the server
|
19
|
+
[
|
20
|
+
Test::Unit::UI::TestRunnerMediator::STARTED,
|
21
|
+
Test::Unit::UI::TestRunnerMediator::FINISHED,
|
22
|
+
Test::Unit::TestResult::FAULT,
|
23
|
+
Test::Unit::TestCase::STARTED,
|
24
|
+
Test::Unit::TestCase::FINISHED,
|
25
|
+
].each do |event|
|
26
|
+
@mediator.add_listener(event) do |*args|
|
27
|
+
Net::HTTP.post_form(mothership_uri('test_event'), {:event => event, :args => args, :guid => @guid})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def connect_to_mothership
|
35
|
+
# TODO probably don't want to abort tests if can't connect, maybe just warn
|
36
|
+
raise 'could not communicate with mothership' unless mothership_alive?
|
37
|
+
end
|
38
|
+
|
39
|
+
def mothership_alive?
|
40
|
+
Net::HTTP.get(mothership_uri('ping'))
|
41
|
+
true
|
42
|
+
rescue
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO memoize
|
47
|
+
def mothership_uri(method)
|
48
|
+
URI.parse("http://localhost:9020/#{method}")
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
# Register myself
|
54
|
+
require 'test/unit/autorunner'
|
55
|
+
Test::Unit::AutoRunner::RUNNERS[:lab_bench] = proc do |r|
|
56
|
+
LabBench::TestRunner
|
57
|
+
end
|
data/lib/lab_bench.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'lab_bench'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lab_bench
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Bradley Buda
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-12-03 00:00:00 -08:00
|
19
|
+
default_executable: lab_bench_server
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
prerelease: false
|
23
|
+
type: :development
|
24
|
+
name: shoulda
|
25
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
requirement: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
prerelease: false
|
37
|
+
type: :development
|
38
|
+
name: bundler
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 23
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 0
|
48
|
+
- 0
|
49
|
+
version: 1.0.0
|
50
|
+
requirement: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
prerelease: false
|
53
|
+
type: :development
|
54
|
+
name: jeweler
|
55
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 1
|
61
|
+
segments:
|
62
|
+
- 1
|
63
|
+
- 5
|
64
|
+
- 1
|
65
|
+
version: 1.5.1
|
66
|
+
requirement: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
prerelease: false
|
69
|
+
type: :development
|
70
|
+
name: rcov
|
71
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
requirement: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
prerelease: false
|
83
|
+
type: :runtime
|
84
|
+
name: sinatra
|
85
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ~>
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 19
|
91
|
+
segments:
|
92
|
+
- 1
|
93
|
+
- 1
|
94
|
+
- 0
|
95
|
+
version: 1.1.0
|
96
|
+
requirement: *id005
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
prerelease: false
|
99
|
+
type: :runtime
|
100
|
+
name: eventmachine
|
101
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ~>
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
hash: 59
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
- 12
|
110
|
+
- 10
|
111
|
+
version: 0.12.10
|
112
|
+
requirement: *id006
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
prerelease: false
|
115
|
+
type: :runtime
|
116
|
+
name: em-websocket
|
117
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ~>
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
hash: 23
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
- 2
|
126
|
+
- 0
|
127
|
+
version: 0.2.0
|
128
|
+
requirement: *id007
|
129
|
+
- !ruby/object:Gem::Dependency
|
130
|
+
prerelease: false
|
131
|
+
type: :runtime
|
132
|
+
name: thin
|
133
|
+
version_requirements: &id008 !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
hash: 31
|
139
|
+
segments:
|
140
|
+
- 1
|
141
|
+
- 2
|
142
|
+
- 0
|
143
|
+
version: 1.2.0
|
144
|
+
requirement: *id008
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
prerelease: false
|
147
|
+
type: :runtime
|
148
|
+
name: yajl-ruby
|
149
|
+
version_requirements: &id009 !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ~>
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
hash: 19
|
155
|
+
segments:
|
156
|
+
- 0
|
157
|
+
- 7
|
158
|
+
- 8
|
159
|
+
version: 0.7.8
|
160
|
+
requirement: *id009
|
161
|
+
description: Like autotest, but in your browser. Currently unsuitable for use by anyone.
|
162
|
+
email: bradleybuda@gmail.com
|
163
|
+
executables:
|
164
|
+
- lab_bench_server
|
165
|
+
extensions: []
|
166
|
+
|
167
|
+
extra_rdoc_files:
|
168
|
+
- LICENSE.txt
|
169
|
+
- README.rdoc
|
170
|
+
files:
|
171
|
+
- .document
|
172
|
+
- .rvmrc
|
173
|
+
- Gemfile
|
174
|
+
- Gemfile.lock
|
175
|
+
- LICENSE.txt
|
176
|
+
- README.rdoc
|
177
|
+
- Rakefile
|
178
|
+
- VERSION
|
179
|
+
- assets/css/app.css
|
180
|
+
- assets/images/spinner.gif
|
181
|
+
- assets/index.html
|
182
|
+
- assets/js/app.js
|
183
|
+
- assets/js/head.min.js
|
184
|
+
- assets/js/mustache.js
|
185
|
+
- bin/lab_bench_server
|
186
|
+
- lib/lab_bench.rb
|
187
|
+
- lib/lab_bench/test_runner.rb
|
188
|
+
- test/helper.rb
|
189
|
+
- test/test_lab_bench.rb
|
190
|
+
has_rdoc: true
|
191
|
+
homepage: http://github.com/bradleybuda/lab_bench
|
192
|
+
licenses:
|
193
|
+
- MIT
|
194
|
+
post_install_message:
|
195
|
+
rdoc_options: []
|
196
|
+
|
197
|
+
require_paths:
|
198
|
+
- lib
|
199
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
200
|
+
none: false
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
hash: 3
|
205
|
+
segments:
|
206
|
+
- 0
|
207
|
+
version: "0"
|
208
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
|
+
none: false
|
210
|
+
requirements:
|
211
|
+
- - ">="
|
212
|
+
- !ruby/object:Gem::Version
|
213
|
+
hash: 3
|
214
|
+
segments:
|
215
|
+
- 0
|
216
|
+
version: "0"
|
217
|
+
requirements: []
|
218
|
+
|
219
|
+
rubyforge_project:
|
220
|
+
rubygems_version: 1.3.7
|
221
|
+
signing_key:
|
222
|
+
specification_version: 3
|
223
|
+
summary: A browser-based runner for Test::Unit
|
224
|
+
test_files:
|
225
|
+
- test/helper.rb
|
226
|
+
- test/test_lab_bench.rb
|