lab_bench 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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
@@ -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 "&amp;";
250
+ case "\\": return "\\\\";
251
+ case '"': return '&quot;';
252
+ case "'": return '&#39;';
253
+ case "<": return "&lt;";
254
+ case ">": return "&gt;";
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
@@ -0,0 +1,6 @@
1
+ module LabBench
2
+ ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+ ASSETS = File.join(ROOT, 'assets')
4
+ end
5
+
6
+ require 'lab_bench/test_runner'
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
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestLabBench < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ 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