├── .jshintrc
├── lib
├── rspectacles.rb
└── rspectacles
│ ├── version.rb
│ ├── app
│ ├── models
│ │ ├── example.rb
│ │ └── run.rb
│ ├── public
│ │ ├── js
│ │ │ ├── script.js
│ │ │ ├── form.js
│ │ │ ├── details.js
│ │ │ ├── exampleStream.js
│ │ │ ├── pathtree.js
│ │ │ ├── zoomable.js
│ │ │ ├── chart.js
│ │ │ ├── riffle.js
│ │ │ ├── mustache.js
│ │ │ └── plates.js
│ │ └── css
│ │ │ └── style.css
│ ├── helpers.rb
│ └── views
│ │ ├── runs.erb
│ │ └── index.erb
│ ├── formatter
│ ├── batched.rb
│ └── base.rb
│ ├── config
│ └── database.yml
│ ├── adapter
│ ├── batched_logger.rb
│ └── logger.rb
│ ├── config.rb
│ └── app.rb
├── viz.png
├── Gemfile
├── .gitignore
├── bin
└── console
├── puma.rb
├── config.ru
├── Procfile
├── Rakefile
├── db
├── migrate
│ ├── 20170907205819_create_examples_table.rb
│ ├── 20170912153508_create_runs.rb
│ └── 20170912175547_associate_runs.rb
└── schema.rb
├── spec
└── javascripts
│ ├── tests
│ └── pathtree_spec.js
│ ├── test.html
│ └── resources
│ ├── qunit.css
│ └── qunit.js
├── .rubocop.yml
├── LICENSE
├── rspectacles.gemspec
├── Gemfile.lock
└── README.md
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "laxcomma": true
3 | }
4 |
--------------------------------------------------------------------------------
/lib/rspectacles.rb:
--------------------------------------------------------------------------------
1 | require 'rspectacles/app.rb'
2 |
--------------------------------------------------------------------------------
/viz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/g2crowd/rspectacles/HEAD/viz.png
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | ruby "2.6.6"
3 |
4 | gemspec
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*
2 | .ruby-gemset
3 | .ruby-version
4 | tmp/*
5 | .bundle/*
6 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | bundle exec irb -r rspectacles/app
4 |
--------------------------------------------------------------------------------
/lib/rspectacles/version.rb:
--------------------------------------------------------------------------------
1 | module RSpectacles
2 | VERSION = '0.5.0'
3 | end
4 |
--------------------------------------------------------------------------------
/puma.rb:
--------------------------------------------------------------------------------
1 | preload_app!
2 | on_worker_boot do
3 | ActiveRecord::Base.establish_connection
4 | end
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift ::File.expand_path(::File.dirname(__FILE__) + '/lib')
2 | require 'rspectacles/app'
3 |
4 | run RSpectacles::App
5 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec puma -t ${PUMA_MIN_THREADS:-1}:${PUMA_MAX_THREADS:-1} -w ${PUMA_WORKERS:-1} -p $PORT -e ${RACK_ENV:-development} -C puma.rb
2 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'sinatra/activerecord/rake'
2 | require 'bundler/gem_tasks'
3 |
4 | namespace :db do
5 | task :load_config do
6 | require './lib/rspectacles/app'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/models/example.rb:
--------------------------------------------------------------------------------
1 | class Example < ActiveRecord::Base
2 | belongs_to :run, primary_key: :rspec_run, foreign_key: :rspec_run
3 | serialize :properties, Hash
4 |
5 | def as_json(*_)
6 | properties
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/models/run.rb:
--------------------------------------------------------------------------------
1 | class Run < ActiveRecord::Base
2 | has_many :examples, primary_key: :rspec_run, foreign_key: :rspec_run
3 |
4 | def total_count
5 | @total_count ||= examples.size
6 | end
7 |
8 | def runtime
9 | @runtime ||= examples.sum(:duration)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20170907205819_create_examples_table.rb:
--------------------------------------------------------------------------------
1 | class CreateExamplesTable < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :examples do |t|
4 | t.string :rspec_run, null: false
5 | t.text :properties
6 | end
7 |
8 | add_index :examples, :rspec_run
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/script.js:
--------------------------------------------------------------------------------
1 | /*global require: true */
2 | require(['zoomable', 'exampleStream'], function (chart, examples) {
3 | 'use strict';
4 |
5 | var
6 | bodyEl = document.querySelector('body')
7 | , ajaxUri = bodyEl.dataset.ajaxUrl
8 | , streams = examples(ajaxUri)
9 | , c = chart()
10 | ;
11 |
12 | streams.example.onOutput(function (data) {
13 | c.push(data);
14 | });
15 |
16 | c.render();
17 | window.c = c;
18 | });
19 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/helpers.rb:
--------------------------------------------------------------------------------
1 | RSpectacles::App.helpers do
2 | def versioned_stylesheet(stylesheet)
3 | checksum = File.mtime(File.join(public_dir, 'css', "#{stylesheet}.css")).to_i
4 | url "/css/#{stylesheet}.css?#{checksum}"
5 | end
6 |
7 | def versioned_javascript(js)
8 | checksum = File.mtime(File.join(public_dir, 'js', "#{js}.js")).to_i
9 | url "/js/#{js}.js?#{checksum}"
10 | end
11 |
12 | def public_dir
13 | File.expand_path('../../app/public', __FILE__)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/form.js:
--------------------------------------------------------------------------------
1 | define(function () {
2 | return function () {
3 | var isCount = false;
4 | var me;
5 |
6 | d3.selectAll("input").on("change", function () {
7 | var newCount = this.value === 'count';
8 |
9 | if (newCount !== isCount) {
10 | isCount = newCount;
11 | me.onChange(isCount);
12 | }
13 | });
14 |
15 | return me = {
16 | isCount: function () {
17 | return isCount;
18 | },
19 |
20 | onChange: function () {
21 | }
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/db/migrate/20170912153508_create_runs.rb:
--------------------------------------------------------------------------------
1 | require 'rspectacles/app/models/example'
2 | require 'rspectacles/app/models/run'
3 |
4 | class CreateRuns < ActiveRecord::Migration[5.1]
5 | def change
6 | create_table :runs do |t|
7 | t.integer :total_time
8 | t.string :rspec_run, null: false
9 |
10 | t.timestamps
11 | end
12 |
13 | add_index :runs, :rspec_run, unique: true
14 | add_reference :examples, :run
15 |
16 | Example.all.distinct.pluck(:rspec_run).each do |run|
17 | Run.where(rspec_run: run).first_or_create
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/db/migrate/20170912175547_associate_runs.rb:
--------------------------------------------------------------------------------
1 | class AssociateRuns < ActiveRecord::Migration[5.1]
2 | def up
3 | add_column :examples, :duration, :numeric, null: false, default: 0
4 | remove_column :runs, :total_time
5 |
6 | Run.all.find_each do |r|
7 | r.update_attributes id: r.rspec_run
8 | end
9 |
10 | count = Example.all.size
11 | Example.all.find_each do |e|
12 | e.update_column :duration, e.properties['duration'].to_f
13 |
14 | count -= 1
15 | puts count
16 | end
17 | end
18 |
19 | def down
20 | remove_column :examples, :duration
21 | add_column :runs, :total_time, :integer
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/javascripts/tests/pathtree_spec.js:
--------------------------------------------------------------------------------
1 | /*global test:true require:true it:true ok:true module:true */
2 |
3 | require(['pathtree'], function (PathTree) {
4 | 'use strict';
5 |
6 | test('constructor', function () {
7 | var tree = new PathTree([{ file_path: './file', full_description: 'hi' }]);
8 | equal(tree.nodes.children[0].name, 'file');
9 | });
10 |
11 | module('PathTree#add', {
12 | setup: function () {
13 | this.tree = new PathTree();
14 | }
15 | });
16 |
17 | test('add', function () {
18 | this.tree.add({ file_path: './file', full_description: 'hi' });
19 | equal(this.tree.nodes.children[0].name, 'file');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/spec/javascripts/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QUnit Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/rspectacles/formatter/batched.rb:
--------------------------------------------------------------------------------
1 | require 'rspectacles/adapter/batched_logger'
2 | require 'rspectacles/formatter/base'
3 |
4 | module RSpectacles
5 | module Formatter
6 | class Batched < RSpectacles::Formatter::Base
7 | RSpec::Core::Formatters.register self,
8 | *%i(example_passed
9 | example_failed
10 | start
11 | stop
12 | message)
13 |
14 | def logger
15 | @logger ||= RSpectacles::Adapter::BatchedLogger.new(test_run_key: current_run_key)
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/rspectacles/config/database.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: postgresql
3 | database: rspectacles_dev
4 | username: postgres
5 | pool: 5
6 | host: localhost
7 | timeout: 5000
8 | variables:
9 | statement_timeout: 3600000
10 |
11 | test: &test
12 | adapter: <%= ENV['DB_ADAPTER'] || 'postgresql' %>
13 | database: rspectacles_test
14 | username: postgres
15 | pool: 5
16 | host: <%= ENV['DB_HOST'] || 'localhost' %>
17 | timeout: 5000
18 | variables:
19 | statement_timeout: 3600000
20 |
21 | production: &production
22 | adapter: postgresql
23 | database: rspectacles_prod
24 | username: postgres
25 | pool: 2
26 | host: localhost
27 | timeout: 5000
28 | variables:
29 | statement_timeout: 3600000
30 |
--------------------------------------------------------------------------------
/lib/rspectacles/adapter/batched_logger.rb:
--------------------------------------------------------------------------------
1 | require 'rspectacles/adapter/logger'
2 |
3 | module RSpectacles
4 | module Adapter
5 | class BatchedLogger < Logger
6 | def queued_messages
7 | @queued_messages ||= []
8 | end
9 |
10 | def stop
11 | super
12 | flush_queue
13 | end
14 |
15 | def batch_size
16 | config.batch_size
17 | end
18 |
19 | def queue(message)
20 | return unless active?
21 | queued_messages << message
22 | flush_queue if queued_messages.count > batch_size
23 | end
24 |
25 | def flush_queue
26 | return unless active?
27 | return unless queued_messages.size > 0
28 |
29 | post_results queued_messages
30 | @queued_messages = []
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/css/style.css:
--------------------------------------------------------------------------------
1 | table {
2 | }
3 |
4 | th, td {
5 | text-align: left;
6 | padding: 8px 16px;
7 | }
8 |
9 | ul {
10 | border: 1px solid #eee;
11 | list-style-type: none;
12 | margin: 0;
13 | padding: 15px;
14 | font-size: 12px;
15 | }
16 |
17 | li {
18 | margin: 0;
19 | padding: 0;
20 | }
21 |
22 | .failed { color: red; }
23 | .passed { color: green; }
24 |
25 | body {
26 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
27 | margin: auto;
28 | position: relative;
29 | width: 960px;
30 | }
31 |
32 | form {
33 | position: absolute;
34 | right: 10px;
35 | top: 10px;
36 | }
37 |
38 | .example-details {
39 | position: fixed;
40 | top: 0;
41 | left: 0;
42 | background: rgba(255, 255, 255, 0.5);
43 | width: 100%
44 | z-index: 100;
45 | overflow: auto;
46 | max-width: 350px;
47 | }
48 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/views/runs.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | RSpectacles
6 | ' />
7 |
8 |
9 |
10 |
11 | | Run |
12 | Examples |
13 | Time |
14 | Created At |
15 |
16 | <% @runs.each do |run| %>
17 |
18 | |
19 | Run #<%= run.rspec_run %>
20 | |
21 |
22 | <%= run.examples.size %>
23 | |
24 |
25 | <% mm, ss = run.runtime.divmod(60) %>
26 | <%= mm.to_i %>:<%= ss.to_i.to_s.rjust(2, '0') %>
27 | |
28 |
29 | <%= run.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
30 | |
31 |
32 | <% end %>
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | Exclude:
3 | - db/schema.rb
4 | - db/migrate/*
5 |
6 | Documentation:
7 | Enabled: false
8 |
9 | LineLength:
10 | Max: 120
11 |
12 | MethodLength:
13 | Max: 15
14 |
15 | ClassLength:
16 | Max: 500
17 |
18 | SpaceBeforeFirstArg:
19 | Enabled: false
20 |
21 | AssignmentInCondition:
22 | Enabled: false
23 |
24 | SingleLineBlockParams:
25 | Enabled: false
26 |
27 | Lambda:
28 | Enabled: false
29 |
30 | NumericLiterals:
31 | Enabled: false
32 |
33 | LambdaCall:
34 | Enabled: false
35 |
36 | AndOr:
37 | Enabled: false
38 |
39 | MultilineOperationIndentation:
40 | Enabled: false
41 |
42 | AbcSize:
43 | Max: 20
44 |
45 | Style/MultilineMethodCallIndentation:
46 | EnforcedStyle: indented_relative_to_receiver
47 |
48 | Bundler/OrderedGems:
49 | Enabled: false
50 |
51 | Metrics/BlockLength:
52 | Enabled: false
53 |
54 | Style/EmptyMethod:
55 | Enabled: false
56 |
57 | Style/FrozenStringLiteralComment:
58 | Enabled: false
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2013 Michael Wheeler
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.
21 |
--------------------------------------------------------------------------------
/rspectacles.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path('../lib', __FILE__)
2 | require 'rspectacles/version'
3 |
4 | Gem::Specification.new do |s|
5 | s.name = 'rspectacles'
6 | s.version = RSpectacles::VERSION
7 | s.authors = ['Michael Wheeler']
8 | s.email = ['mwheeler@g2crowd.com']
9 | s.homepage = 'https://github.com/g2crowd/rspectacles'
10 | s.summary = 'Visualize rspec test running in the browser'
11 | s.description = 'Visualize rspec test running in the browser'
12 | s.license = 'MIT'
13 |
14 | s.required_ruby_version = '>= 2.6.6'
15 |
16 | s.files = `git ls-files`.split("\n")
17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
19 | s.require_paths = ['lib']
20 |
21 | # specify any dependencies here; for example:
22 | s.add_development_dependency 'pry'
23 | s.add_development_dependency 'rspec'
24 | s.add_dependency 'httparty'
25 | s.add_dependency 'pg'
26 | s.add_dependency 'puma'
27 | s.add_dependency 'rack', '>= 2.0.6'
28 | s.add_dependency 'rake'
29 | s.add_dependency 'sinatra', '>= 1.4.5'
30 | s.add_dependency 'sinatra-activerecord'
31 | end
32 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/details.js:
--------------------------------------------------------------------------------
1 | define(['jquery', 'mustache'], function($, Mustache) {
2 | var me,
3 | defaults,
4 | current = {},
5 | tmpl = $('#template').html(),
6 | isCount = false;
7 |
8 | defaults = function() {
9 | return {
10 | name: '',
11 | line_number: '',
12 | status: '',
13 | duration: '',
14 | time_or_count: isCount ? 'Examples' : 'ms',
15 | minutes: '',
16 | value: null
17 | };
18 | };
19 |
20 | function secToMin(time) {
21 | var pad = function(val) {
22 | return ('00' + val).slice(-2);
23 | },
24 | min = parseInt(time / 60),
25 | sec = parseInt(time % 60);
26 |
27 | return pad(min) + ':' + pad(sec);
28 | }
29 |
30 | function render() {
31 | if (current.value) {
32 | if (!isCount) {
33 | current.minutes = secToMin(current.value);
34 | current.value = parseInt(current.value * 1000);
35 | }
36 | }
37 |
38 | $('.example-wrapper').html(Mustache.render(tmpl, current));
39 | }
40 |
41 | return {
42 | update: function(d) {
43 | current = $.extend({}, defaults(), d);
44 | render();
45 | },
46 |
47 | isCount: function(value) {
48 | isCount = value;
49 | },
50 | };
51 | });
52 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/exampleStream.js:
--------------------------------------------------------------------------------
1 | /*global define: true EventSource: true */
2 | define(['riffle'], function (riffle) {
3 | 'use strict';
4 |
5 | return function streams(ajaxUri) {
6 | var stream = riffle.stream
7 | , each
8 | , jsonEvents
9 | ;
10 |
11 | function ajaxStream(url, args) {
12 | return stream(function (o) {
13 | $.get(url, args, function (d) {
14 | o(JSON.parse(d));
15 | });
16 | });
17 | }
18 |
19 | each = stream(function (o, i) {
20 | i.forEach(function (item) { o(item); });
21 | });
22 |
23 | function batched(delay, maxSize) {
24 | var batch = []
25 | , timer
26 | ;
27 |
28 | delay = delay || 100;
29 | maxSize = maxSize || 100;
30 |
31 | function clear(o) {
32 | if (batch.length > 0) {
33 | o(batch.splice(0, maxSize));
34 | }
35 |
36 | if (batch.length < 1) {
37 | clearInterval(timer);
38 | timer = null;
39 | }
40 | }
41 |
42 | return stream(function (o, i) {
43 | batch = batch.concat(i);
44 |
45 | if (!timer) { timer = setInterval(function () { clear(o); }, delay); }
46 | });
47 | }
48 |
49 | jsonEvents = each.input(ajaxStream(ajaxUri).invoke());
50 |
51 | return { example: batched(10, 1000).input(jsonEvents) };
52 | };
53 | });
54 |
--------------------------------------------------------------------------------
/lib/rspectacles/formatter/base.rb:
--------------------------------------------------------------------------------
1 | require 'rspectacles/adapter/logger'
2 |
3 | module RSpectacles
4 | module Formatter
5 | class Base
6 | RSpec::Core::Formatters.register self,
7 | :example_passed,
8 | :example_failed,
9 | :start,
10 | :stop,
11 | :message
12 |
13 | attr_reader :output
14 |
15 | def initialize(_)
16 | end
17 |
18 | def logger
19 | @logger ||= RSpectacles::Adapter::Logger.new(test_run_key: current_run_key)
20 | end
21 |
22 | def message(_notification)
23 | end
24 |
25 | def start(_)
26 | logger.start
27 | end
28 |
29 | def stop(_)
30 | logger.stop
31 | end
32 |
33 | def example_passed(notification)
34 | logger.log notification.example
35 | end
36 |
37 | def example_pending(notification)
38 | logger.log notification.example
39 | end
40 |
41 | def example_failed(notification)
42 | logger.log notification.example
43 | end
44 |
45 | def current_run_key
46 | ENV['CURRENT_RSPEC_RUN'] || config.last_run_primary_key
47 | end
48 |
49 | def config
50 | RSpectacles.config
51 | end
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/pathtree.js:
--------------------------------------------------------------------------------
1 | // @format
2 | /*global define: true */
3 | define(['jquery'], function($) {
4 | 'use strict';
5 |
6 | function createNodes(path, data, node) {
7 | var nextNode;
8 |
9 | node.children = node.children || [];
10 |
11 | if (path.length === 0) {
12 | nextNode = $.extend(
13 | {
14 | size: data.duration,
15 | name: data.full_description,
16 | },
17 | data,
18 | );
19 |
20 | node.children.push(nextNode);
21 | return;
22 | }
23 |
24 | nextNode = node.children.filter(function(child) {
25 | return child.name === path[0];
26 | })[0];
27 |
28 | if (!nextNode) {
29 | nextNode = {name: path[0]};
30 | node.children.push(nextNode);
31 | }
32 |
33 | path.shift();
34 | createNodes(path, data, nextNode);
35 | }
36 |
37 | function PathTree(data) {
38 | this.nodes = {};
39 | data && this.add(data);
40 | }
41 |
42 | PathTree.prototype.add = function(data) {
43 | var that = this;
44 | !$.isArray(data) && (data = [data]);
45 |
46 | data.forEach(function(node) {
47 | var path = node.file_path.split('/');
48 |
49 | ['.'].forEach(function(v) {
50 | if (path[0] === v) {
51 | path.shift();
52 | }
53 | });
54 |
55 | createNodes(path, node, that.nodes);
56 | });
57 | };
58 |
59 | return PathTree;
60 | });
61 |
--------------------------------------------------------------------------------
/lib/rspectacles/config.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 | require 'yaml'
3 | require 'erb'
4 | require 'securerandom'
5 |
6 | module RSpectacles
7 | class Config
8 | def initialize
9 | @opts = OpenStruct.new defaults.merge(yml)
10 | end
11 |
12 | def defaults
13 | {
14 | batch_size: (ENV['RSPECTACLES_BATCH_SIZE'] || 1000).to_i,
15 | last_run_primary_key: ENV['RSPECTACLES_RUN_KEY'] || ENV['CIRCLE_BUILD_NUM'] || SecureRandom.hex,
16 | timeout: (ENV['RSPECTACLES_TIMEOUT'] || 15).to_i,
17 | rspectacles_url: ENV['RSPECTACLES_URL']
18 | }
19 | end
20 |
21 | def timeout
22 | @opts[:timeout]
23 | end
24 |
25 | def method_missing(method, *args)
26 | @opts.public_send method, *args
27 | end
28 |
29 | private
30 |
31 | def yml_path
32 | ::File.expand_path(ENV['RSPECTACLES_CONFIG']) if ENV['RSPECTACLES_CONFIG']
33 | end
34 |
35 | def yml_exists?
36 | yml_path && ::File.exist?(yml_path)
37 | end
38 |
39 | def yml
40 | res = if yml_exists?
41 | @yml ||= ::YAML.safe_load(::ERB.new(IO.read(yml_path)).result)
42 | else
43 | {}
44 | end
45 |
46 | sanitize(res)
47 | end
48 |
49 | def sanitize(hash)
50 | hash.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = value }
51 | end
52 | end
53 |
54 | def self.configuration
55 | @configuration ||= Config.new
56 | end
57 |
58 | def self.config
59 | yield configuration if block_given?
60 | configuration
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20170912175547) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "examples", force: :cascade do |t|
19 | t.string "rspec_run", null: false
20 | t.text "properties"
21 | t.bigint "run_id"
22 | t.decimal "duration", default: "0.0", null: false
23 | t.index ["rspec_run"], name: "index_examples_on_rspec_run"
24 | t.index ["run_id"], name: "index_examples_on_run_id"
25 | end
26 |
27 | create_table "runs", force: :cascade do |t|
28 | t.string "rspec_run", null: false
29 | t.datetime "created_at", null: false
30 | t.datetime "updated_at", null: false
31 | t.index ["rspec_run"], name: "index_runs_on_rspec_run", unique: true
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/views/index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | RSpectacles
6 | ' />
7 |
8 | >
9 |
13 |
14 |
15 |
16 |
17 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/lib/rspectacles/app.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sinatra/base'
3 | require 'json'
4 | require 'sinatra/activerecord'
5 | require 'rspectacles/config.rb'
6 | require 'rspectacles/app/models/example'
7 | require 'rspectacles/app/models/run'
8 | require 'puma'
9 |
10 | module RSpectacles
11 | class App < Sinatra::Base
12 | require 'rspectacles/app/helpers'
13 | register Sinatra::ActiveRecordExtension
14 |
15 | set :database_file, 'config/database.yml'
16 |
17 | config = RSpectacles.config
18 | dir = File.dirname(File.expand_path(__FILE__))
19 | set :app_file, __FILE__
20 | set :root, dir
21 | set :views, "#{dir}/app/views"
22 | set :static, true
23 |
24 | if respond_to? :public_folder
25 | set :public_folder, "#{dir}/app/public"
26 | else
27 | set :public, "#{dir}/app/public"
28 | end
29 |
30 | # Routes
31 | get '/' do
32 | @runs = Run.all.limit(25).order(created_at: :desc)
33 | erb :runs
34 | end
35 |
36 | get '/watch/:key' do
37 | erb :index
38 | end
39 |
40 | get '/watch' do
41 | params['key'] = config.last_run_primary_key
42 | erb :index
43 | end
44 |
45 | get '/examples/:key' do
46 | Example.where(rspec_run: params['key']).to_json
47 | end
48 |
49 | post '/examples' do
50 | payload = JSON.parse(request.body.read)
51 |
52 | Run.where(rspec_run: payload['examples'].first['rspec_run']).first_or_create
53 |
54 | data = payload['examples'].map do |args|
55 | { rspec_run: args['rspec_run'], duration: args['duration'].to_f, properties: args }
56 | end
57 |
58 | examples = Example.create(data)
59 |
60 | { errors: examples.count { |i| !i.persisted? } }.to_json
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/rspectacles/adapter/logger.rb:
--------------------------------------------------------------------------------
1 | require 'rspectacles/config'
2 | require 'uri'
3 | require 'json'
4 | require 'httparty'
5 |
6 | module RSpectacles
7 | module Adapter
8 | class Logger
9 | attr_reader :test_run_key
10 |
11 | def initialize(test_run_key: nil)
12 | @test_run_key = test_run_key || config.last_run_primary_key
13 | end
14 |
15 | def config
16 | RSpectacles.config
17 | end
18 |
19 | def uri
20 | @uri ||= config.rspectacles_url
21 | end
22 |
23 | def stop
24 | end
25 |
26 | def start
27 | end
28 |
29 | def log(example)
30 | message = format_example(example)
31 | queue message
32 | end
33 |
34 | private
35 |
36 | def queue(message)
37 | return unless active?
38 | post_results Array.wrap(message)
39 | end
40 |
41 | def post_results(messages)
42 | HTTParty.post(full_uri, timeout: config.timeout,
43 | body: { examples: messages }.to_json,
44 | headers: { 'Content-Type' => 'application/json' })
45 | rescue Net::ReadTimeout
46 | puts "RSpectacles Timeout! Failed to send #{messages.size} messages"
47 | end
48 |
49 | def active?
50 | !!uri
51 | end
52 |
53 | def full_uri
54 | "#{uri}/examples"
55 | end
56 |
57 | def format_example(example)
58 | {
59 | rspec_run: test_run_key,
60 | description: example.description,
61 | full_description: example.full_description,
62 | status: example.execution_result.status,
63 | duration: example.execution_result.run_time,
64 | file_path: example.metadata[:file_path],
65 | line_number: example.metadata[:line_number]
66 | }
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | rspectacles (0.5.0)
5 | httparty
6 | pg
7 | puma
8 | rack (>= 2.0.6)
9 | rake
10 | sinatra (>= 1.4.5)
11 | sinatra-activerecord
12 |
13 | GEM
14 | remote: https://rubygems.org/
15 | specs:
16 | activemodel (6.1.3)
17 | activesupport (= 6.1.3)
18 | activerecord (6.1.3)
19 | activemodel (= 6.1.3)
20 | activesupport (= 6.1.3)
21 | activesupport (6.1.3)
22 | concurrent-ruby (~> 1.0, >= 1.0.2)
23 | i18n (>= 1.6, < 2)
24 | minitest (>= 5.1)
25 | tzinfo (~> 2.0)
26 | zeitwerk (~> 2.3)
27 | coderay (1.1.1)
28 | concurrent-ruby (1.1.8)
29 | diff-lcs (1.1.3)
30 | httparty (0.16.3)
31 | mime-types (~> 3.0)
32 | multi_xml (>= 0.5.2)
33 | i18n (1.8.9)
34 | concurrent-ruby (~> 1.0)
35 | method_source (0.8.2)
36 | mime-types (3.2.2)
37 | mime-types-data (~> 3.2015)
38 | mime-types-data (3.2018.0812)
39 | minitest (5.14.4)
40 | multi_xml (0.6.0)
41 | mustermann (1.1.1)
42 | ruby2_keywords (~> 0.0.1)
43 | nio4r (2.5.8)
44 | pg (1.1.3)
45 | pry (0.10.4)
46 | coderay (~> 1.1.0)
47 | method_source (~> 0.8.1)
48 | slop (~> 3.4)
49 | puma (5.6.4)
50 | nio4r (~> 2.0)
51 | rack (2.2.3)
52 | rack-protection (2.2.0)
53 | rack
54 | rake (13.0.1)
55 | rspec (2.12.0)
56 | rspec-core (~> 2.12.0)
57 | rspec-expectations (~> 2.12.0)
58 | rspec-mocks (~> 2.12.0)
59 | rspec-core (2.12.2)
60 | rspec-expectations (2.12.1)
61 | diff-lcs (~> 1.1.3)
62 | rspec-mocks (2.12.2)
63 | ruby2_keywords (0.0.5)
64 | sinatra (2.2.0)
65 | mustermann (~> 1.0)
66 | rack (~> 2.2)
67 | rack-protection (= 2.2.0)
68 | tilt (~> 2.0)
69 | sinatra-activerecord (2.0.18)
70 | activerecord (>= 4.1)
71 | sinatra (>= 1.0)
72 | slop (3.6.0)
73 | tilt (2.0.10)
74 | tzinfo (2.0.4)
75 | concurrent-ruby (~> 1.0)
76 | zeitwerk (2.4.2)
77 |
78 | PLATFORMS
79 | ruby
80 |
81 | DEPENDENCIES
82 | pry
83 | rspec
84 | rspectacles!
85 |
86 | RUBY VERSION
87 | ruby 2.6.6p146
88 |
89 | BUNDLED WITH
90 | 2.1.4
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://badge.fury.io/rb/rspectacles)
2 | # RSpectacles
3 |
4 | RSpectacles is an in-browser visualizer and profiler for RSpec. It uses d3.js to render a
5 | [partition](http://bl.ocks.org/mbostock/4063423) of your specs based on time to complete, so
6 | that you can tell at a glance where the time is spent in your test suite.
7 |
8 | 
9 |
10 | As a Sinatra app it can be run standalone, or else mounted on another Rack app.
11 |
12 | ## Compatibility
13 |
14 | RSpectacles assumes you are using RSpec 3 or later.
15 |
16 | ## Installation
17 |
18 | gem install rspectacles
19 |
20 | Or in your Gemfile:
21 |
22 | ```ruby
23 | group :test, :development do
24 | gem 'rspectacles'
25 | end
26 | ```
27 |
28 | Then add the formatter to your `.rspec` file:
29 |
30 | --require rspectacles/formatter/batched
31 | --format RSpectacles::Formatter::Batched
32 |
33 | --format progress # or whatever other formatters you want to use
34 |
35 | The formatter assumes you are using RSpec 3.
36 |
37 | ## Batched Formatter
38 |
39 | The `Batched` formatter is preferred, as it will send fewer web requests and will be less likely to
40 | slow down your specs if the connection to the server is slow. You can change the batch
41 | sizes by changing the `batch_size` in config settings.
42 |
43 | ## Storage
44 |
45 | RSpectacles depends on ActiveRecord for persistence. You
46 | can quickly get an instance up and running by setting the `DATABASE_URL` environment variable,
47 | and running the standard rake commands:
48 |
49 | export DATABASE_URL=postgres://...
50 | rake db:create
51 | rake db:migrate
52 |
53 | Start the server and connect to it in your browser:
54 |
55 | rackup
56 |
57 | Then run your specs and watch the magic happen!
58 |
59 | ## Web Server
60 |
61 | The server uses ActiveRecord and Postgres to store the examples.
62 |
63 | Run migrations:
64 |
65 | # set ENV['DATABASE_URL'] to point to your database, or else database.yml defaults will be used
66 | rake db:create
67 | rake db:migrate
68 |
69 | Start the server:
70 |
71 | puma
72 |
73 | ## Configuration
74 |
75 | Configuration settings can all be set through environment variables:
76 |
77 | | Environment variable | Description | Default value |
78 | | --- | --- | --- |
79 | | RSPECTACLES_URL | Where server is running | |
80 | | RSPECTACLES_RUN_KEY | Set this to log parallel builds to same report | |
81 | | RSPECTACLES_BATCH_SIZE | | 1000 |
82 | | RSPECTACLES_TIMEOUT | | 15 |
83 |
84 | ## Contributing
85 |
86 | 1. Fork it
87 | 2. Create your feature branch (`git checkout -b my-new-feature`)
88 | 3. Commit your changes (`git commit -am 'Added some feature'`)
89 | 4. Push to the branch (`git push origin my-new-feature`)
90 | 5. Create new Pull Request
91 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/zoomable.js:
--------------------------------------------------------------------------------
1 | define(['jquery', 'pathtree', 'details', 'form'], function ($, PathTree, details, countForm) {
2 | return function (options) {
3 | var width = 960,
4 | height = 700,
5 | radius = (Math.min(width, height) / 2) - 10,
6 | form = countForm(),
7 | me;
8 |
9 | function arcTween(a) {
10 | var i = d3.interpolate({ x: a.x0, dx: a.dx0 }, a);
11 | return function tweener(t) {
12 | var b = i(t);
13 | a.x0 = b.x;
14 | a.dx0 = b.dx;
15 | return arc(b);
16 | };
17 | }
18 |
19 | var x = d3.scale.linear()
20 | .range([0, 2 * Math.PI]);
21 |
22 | var y = d3.scale.sqrt()
23 | .range([0, radius]);
24 |
25 | var color = d3.scale.category20c();
26 |
27 | var partition = d3.layout.partition()
28 | .sort(function(a, b) { return d3.ascending(a.name, b.name); })
29 | .value(function(d) { return d.size; });
30 |
31 | var arc = d3.svg.arc()
32 | .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
33 | .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
34 | .innerRadius(function(d) { return Math.max(0, y(d.y)); })
35 | .outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
36 |
37 | var svg = d3.select("body").append("svg")
38 | .attr("width", width)
39 | .attr("height", height)
40 | .append("g")
41 | .attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
42 |
43 | function getValue() {
44 | return form.isCount() ?
45 | function () { return 1; } :
46 | function (d) { return d.size; };
47 | }
48 |
49 | function getColor(d) {
50 | if (d.status && d.status === 'failed') {
51 | return '#f00';
52 | } else {
53 | return color(((d.children ? d : d.parent) || {}).name);
54 | }
55 | }
56 |
57 | function stash(d) {
58 | d.x0 = d.x;
59 | d.dx0 = d.dx;
60 | }
61 |
62 |
63 | function onUpdate(path) {
64 | path
65 | .attr("d", arc)
66 | .each(stash)
67 | .style("fill", getColor);
68 | }
69 |
70 | var render = function () {
71 | var path = svg.datum(me.tree.nodes).selectAll("path")
72 | .data(partition.value(getValue()).nodes);
73 |
74 | onUpdate(path);
75 | path
76 | .enter().append("path")
77 | .on('mouseover', details.update)
78 | .attr("d", arc)
79 | .style("fill", getColor)
80 | .style("stroke", function (d) { return 'rgba(255,255,255,0.3)'; })
81 | .style("fill-rule", "evenodd")
82 | .on("click", click);
83 |
84 | form.onChange = function (isCount) {
85 | details.isCount(isCount);
86 |
87 | path
88 | .data(partition.value(getValue()).nodes)
89 | .transition()
90 | .duration(1500)
91 | .attrTween("d", arcTween);
92 | };
93 |
94 | render = function () {
95 | path.datum(me.tree.nodes)
96 | .data(partition.value(getValue()).nodes);
97 | };
98 | };
99 |
100 | function click(d) {
101 | svg.transition()
102 | .duration(750)
103 | .tween("scale", function() {
104 | var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
105 | yd = d3.interpolate(y.domain(), [d.y, 1]),
106 | yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
107 | return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); };
108 | })
109 | .selectAll("path")
110 | .attrTween("d", function(d) { return function() { return arc(d); }; });
111 | }
112 |
113 | d3.select(self.frameElement).style("height", height + "px");
114 |
115 | return me = {
116 | tree: new PathTree(),
117 |
118 | render: render,
119 |
120 | reset: function () {
121 | me.tree = new PathTree();
122 | me.render();
123 | },
124 |
125 | push: function (data) {
126 | me.tree.add(data);
127 | me.render();
128 | }
129 | };
130 | }
131 | });
132 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/chart.js:
--------------------------------------------------------------------------------
1 | // @format
2 | /*global define: true d3: true */
3 | define(['jquery', 'pathtree', 'details'], function(
4 | $,
5 | PathTree,
6 | details,
7 | ) {
8 | 'use strict';
9 |
10 | function chart(options) {
11 | var svg,
12 | partition,
13 | arc,
14 | me,
15 | tmpl = $('#template').html(),
16 | render;
17 |
18 | options = $.extend(
19 | {
20 | width: 960,
21 | height: 700,
22 | color: d3.scale.category20c(),
23 | isCount: false,
24 | },
25 | options,
26 | );
27 |
28 | options.radius = Math.min(options.width, options.height) / 2;
29 |
30 | svg = d3
31 | .select('body')
32 | .append('svg')
33 | .attr('width', options.width)
34 | .attr('height', options.height)
35 | .append('g')
36 | .attr(
37 | 'transform',
38 | 'translate(' + options.width / 2 + ',' + options.height * 0.52 + ')',
39 | );
40 |
41 | partition = d3.layout
42 | .partition()
43 | .sort(null)
44 | .size([2 * Math.PI, options.radius * options.radius]);
45 |
46 | arc = d3.svg
47 | .arc()
48 | .startAngle(function(d) {
49 | return d.x;
50 | })
51 | .endAngle(function(d) {
52 | return d.x + d.dx;
53 | })
54 | .innerRadius(function(d) {
55 | return Math.sqrt(d.y);
56 | })
57 | .outerRadius(function(d) {
58 | return Math.sqrt(d.y + d.dy);
59 | });
60 |
61 | // Stash the old values for transition.
62 | function stash(d) {
63 | d.x0 = d.x;
64 | d.dx0 = d.dx;
65 | }
66 |
67 | // Interpolate the arcs in data space.
68 | function arcTween(a) {
69 | var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
70 | return function tweener(t) {
71 | var b = i(t);
72 | a.x0 = b.x;
73 | a.dx0 = b.dx;
74 | return arc(b);
75 | };
76 | }
77 |
78 | function secToMin(time) {
79 | var pad = function(val) {
80 | return ('00' + val).slice(-2);
81 | },
82 | min = parseInt(time / 60),
83 | sec = parseInt(time % 60);
84 |
85 | return pad(min) + ':' + pad(sec);
86 | }
87 |
88 | function getValue() {
89 | return options.isCount
90 | ? function() {
91 | return 1;
92 | }
93 | : function(d) {
94 | return d.size;
95 | };
96 | }
97 |
98 | function getColor(d) {
99 | if (d.status && d.status === 'failed') {
100 | return '#f00';
101 | } else {
102 | return options.color(((d.children ? d : d.parent) || {}).name);
103 | }
104 | }
105 |
106 | function onUpdate(path) {
107 | path
108 | .attr('d', arc)
109 | .each(stash)
110 | .style('fill', getColor)
111 | .call(details.update);
112 | }
113 |
114 | function onEnter(path) {
115 | path
116 | .enter()
117 | .append('path')
118 | .attr('display', function(d) {
119 | return d.depth ? null : 'none';
120 | })
121 | .attr('d', arc)
122 | .style('stroke', function(d) {
123 | return 'rgba(255,255,255,0.3)';
124 | })
125 | .style('fill', getColor)
126 | .style('fill-rule', 'evenodd')
127 | .each(stash)
128 | .on('mouseover', details.update)
129 | .call(details.update);
130 | }
131 |
132 | function onExit(path) {
133 | path.exit().remove();
134 | }
135 |
136 | function onFormChange(path) {
137 | d3.selectAll('input').on('change', function change() {
138 | options.isCount = this.value === 'count';
139 |
140 | path
141 | .data(partition.value(getValue()).nodes)
142 | .transition()
143 | .duration(1500)
144 | .attrTween('d', arcTween);
145 | });
146 | }
147 |
148 | render = function() {
149 | var path = svg
150 | .datum(me.tree.nodes)
151 | .selectAll('path')
152 | .data(partition.value(getValue()).nodes);
153 |
154 | onUpdate(path);
155 | onEnter(path);
156 | onExit(path);
157 | onFormChange(path);
158 |
159 | render = function() {
160 | path.datum(me.tree.nodes).data(partition.value(getValue()).nodes);
161 | };
162 | };
163 |
164 | return (me = {
165 | tree: new PathTree(),
166 |
167 | render: render,
168 |
169 | reset: function() {
170 | me.tree = new PathTree();
171 | me.render();
172 | },
173 |
174 | push: function(data) {
175 | me.tree.add(data);
176 | me.render();
177 | },
178 | });
179 | }
180 |
181 | return chart;
182 | });
183 |
--------------------------------------------------------------------------------
/spec/javascripts/resources/qunit.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework
3 | *
4 | * http://qunitjs.com
5 | *
6 | * Copyright 2012 jQuery Foundation and other contributors
7 | * Released under the MIT license.
8 | * http://jquery.org/license
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 5px 5px 0 0;
42 | -moz-border-radius: 5px 5px 0 0;
43 | -webkit-border-top-right-radius: 5px;
44 | -webkit-border-top-left-radius: 5px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-testrunner-toolbar label {
58 | display: inline-block;
59 | padding: 0 .5em 0 .1em;
60 | }
61 |
62 | #qunit-banner {
63 | height: 5px;
64 | }
65 |
66 | #qunit-testrunner-toolbar {
67 | padding: 0.5em 0 0.5em 2em;
68 | color: #5E740B;
69 | background-color: #eee;
70 | overflow: hidden;
71 | }
72 |
73 | #qunit-userAgent {
74 | padding: 0.5em 0 0.5em 2.5em;
75 | background-color: #2b81af;
76 | color: #fff;
77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
78 | }
79 |
80 | #qunit-modulefilter-container {
81 | float: right;
82 | }
83 |
84 | /** Tests: Pass/Fail */
85 |
86 | #qunit-tests {
87 | list-style-position: inside;
88 | }
89 |
90 | #qunit-tests li {
91 | padding: 0.4em 0.5em 0.4em 2.5em;
92 | border-bottom: 1px solid #fff;
93 | list-style-position: inside;
94 | }
95 |
96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
97 | display: none;
98 | }
99 |
100 | #qunit-tests li strong {
101 | cursor: pointer;
102 | }
103 |
104 | #qunit-tests li a {
105 | padding: 0.5em;
106 | color: #c2ccd1;
107 | text-decoration: none;
108 | }
109 | #qunit-tests li a:hover,
110 | #qunit-tests li a:focus {
111 | color: #000;
112 | }
113 |
114 | #qunit-tests li .runtime {
115 | float: right;
116 | font-size: smaller;
117 | }
118 |
119 | .qunit-assert-list {
120 | margin-top: 0.5em;
121 | padding: 0.5em;
122 |
123 | background-color: #fff;
124 |
125 | border-radius: 5px;
126 | -moz-border-radius: 5px;
127 | -webkit-border-radius: 5px;
128 | }
129 |
130 | .qunit-collapsed {
131 | display: none;
132 | }
133 |
134 | #qunit-tests table {
135 | border-collapse: collapse;
136 | margin-top: .2em;
137 | }
138 |
139 | #qunit-tests th {
140 | text-align: right;
141 | vertical-align: top;
142 | padding: 0 .5em 0 0;
143 | }
144 |
145 | #qunit-tests td {
146 | vertical-align: top;
147 | }
148 |
149 | #qunit-tests pre {
150 | margin: 0;
151 | white-space: pre-wrap;
152 | word-wrap: break-word;
153 | }
154 |
155 | #qunit-tests del {
156 | background-color: #e0f2be;
157 | color: #374e0c;
158 | text-decoration: none;
159 | }
160 |
161 | #qunit-tests ins {
162 | background-color: #ffcaca;
163 | color: #500;
164 | text-decoration: none;
165 | }
166 |
167 | /*** Test Counts */
168 |
169 | #qunit-tests b.counts { color: black; }
170 | #qunit-tests b.passed { color: #5E740B; }
171 | #qunit-tests b.failed { color: #710909; }
172 |
173 | #qunit-tests li li {
174 | padding: 5px;
175 | background-color: #fff;
176 | border-bottom: none;
177 | list-style-position: inside;
178 | }
179 |
180 | /*** Passing Styles */
181 |
182 | #qunit-tests li li.pass {
183 | color: #3c510c;
184 | background-color: #fff;
185 | border-left: 10px solid #C6E746;
186 | }
187 |
188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
189 | #qunit-tests .pass .test-name { color: #366097; }
190 |
191 | #qunit-tests .pass .test-actual,
192 | #qunit-tests .pass .test-expected { color: #999999; }
193 |
194 | #qunit-banner.qunit-pass { background-color: #C6E746; }
195 |
196 | /*** Failing Styles */
197 |
198 | #qunit-tests li li.fail {
199 | color: #710909;
200 | background-color: #fff;
201 | border-left: 10px solid #EE5757;
202 | white-space: pre;
203 | }
204 |
205 | #qunit-tests > li:last-child {
206 | border-radius: 0 0 5px 5px;
207 | -moz-border-radius: 0 0 5px 5px;
208 | -webkit-border-bottom-right-radius: 5px;
209 | -webkit-border-bottom-left-radius: 5px;
210 | }
211 |
212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
213 | #qunit-tests .fail .test-name,
214 | #qunit-tests .fail .module-name { color: #000000; }
215 |
216 | #qunit-tests .fail .test-actual { color: #EE5757; }
217 | #qunit-tests .fail .test-expected { color: green; }
218 |
219 | #qunit-banner.qunit-fail { background-color: #EE5757; }
220 |
221 |
222 | /** Result */
223 |
224 | #qunit-testresult {
225 | padding: 0.5em 0.5em 0.5em 2.5em;
226 |
227 | color: #2b81af;
228 | background-color: #D2E0E6;
229 |
230 | border-bottom: 1px solid white;
231 | }
232 | #qunit-testresult .module-name {
233 | font-weight: bold;
234 | }
235 |
236 | /** Fixture */
237 |
238 | #qunit-fixture {
239 | position: absolute;
240 | top: -10000px;
241 | left: -10000px;
242 | width: 1000px;
243 | height: 1000px;
244 | }
245 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/riffle.js:
--------------------------------------------------------------------------------
1 | (function (definition) {
2 | // AMD
3 | if (typeof define === "function") {
4 | define(definition);
5 | // CommonJS
6 | } else if (typeof exports === "object") {
7 | definition(this, exports);
8 | // Browser
9 | } else {
10 | definition(this);
11 | }
12 | }(function (require, exports, module) {
13 | "use strict";
14 | var old,
15 | _;
16 |
17 | exports = exports || window;
18 |
19 | if (!window.setTimeout) {
20 | return;
21 | }
22 |
23 | function stream(userSuppliedStreamFn) {
24 | var chain = {};
25 |
26 | function defaultStreamFn(output) {
27 | var inputs = _.argumentsToArray(arguments);
28 | inputs.shift();
29 | if (inputs.length === 0) {
30 | window.setTimeout(function () {
31 | output();
32 | }, 0);
33 | }
34 | _.each(inputs, function invokeOutput(input) {
35 | if (!_.isUndefined(input)) {
36 | window.setTimeout(function delayedInvokeOutput() {
37 | output(input);
38 | }, 0);
39 | }
40 | });
41 | }
42 |
43 | (function () {
44 | var outputFns = [], streamFn;
45 | function outputAllFns() {
46 | var outputs = _.argumentsToArray(arguments);
47 | _.each(outputFns, function applyFunction(f) {
48 | _.applyArgsToFn(f, outputs);
49 | });
50 | }
51 | streamFn = _.isFunction(userSuppliedStreamFn) ? userSuppliedStreamFn : defaultStreamFn;
52 | chain.invoke = function invoke() {
53 | var outputs = _.argumentsToArray(arguments);
54 | outputs.unshift(outputAllFns);
55 | _.applyArgsToFn(streamFn, outputs);
56 | return chain;
57 | };
58 | chain.onOutput = function onOutput(f) {
59 | if (!_.isFunction(f)) {
60 | throw new Error('onOutput expecting callback function');
61 | }
62 | outputFns.push(f);
63 | return chain;
64 | };
65 | chain.offOutput = function offOutput(f) {
66 | if (!_.isFunction(f)) {
67 | throw new Error('offOutput expecting callback function');
68 | }
69 | outputFns = _.reject(outputFns, function isSameAsReferenceInScope(x) {
70 | return x === f;
71 | });
72 | return chain;
73 | };
74 | }());
75 |
76 | (function () {
77 | var callbacks = [],
78 | inputStreams = [];
79 | function wait(streams, idx) {
80 | var unbindStreams = inputStreams[idx];
81 | function invokeWithOneArg(x) {
82 | var outputs = [];
83 | outputs.length = idx + 1;
84 | outputs[idx] = x;
85 | chain.invoke.apply(window, _.isUndefined(x) ? [] : outputs);
86 | }
87 | if (unbindStreams) {
88 | _.each(unbindStreams, function unbindInputs(s) {
89 | s.offOutput(callbacks[idx]);
90 | });
91 | delete callbacks[idx];
92 | delete inputStreams[idx];
93 | }
94 | _.each(streams, function registerOnOutput(stream) {
95 | stream.onOutput(invokeWithOneArg);
96 | });
97 | callbacks[idx] = invokeWithOneArg;
98 | inputStreams[idx] = streams;
99 | }
100 | chain.input = function input() {
101 | var i, removeStreamIndexes = [];
102 | _.each(arguments, function bindInputs(inputs, inIdx) {
103 | if (stream.isStream(inputs)) {
104 | inputs = [inputs];
105 | }
106 | if (_.isArray(inputs)) {
107 | inputs = _.reject(inputs, function isNotStream(obj) {
108 | return !stream.isStream(obj);
109 | });
110 | wait(inputs, inIdx);
111 | }
112 | });
113 | for (i = arguments.length; i < inputStreams.length; i += 1) {
114 | removeStreamIndexes.push(i);
115 | }
116 | _.each(removeStreamIndexes, function (idx) {
117 | _.each(inputStreams[idx], function (stream) { stream.offOutput(callbacks[idx]); });
118 | delete callbacks[idx];
119 | delete inputStreams[idx];
120 | });
121 | return chain;
122 | };
123 | }());
124 | return chain;
125 | }
126 |
127 | stream.isStream = function isStream(x) {
128 | return !!(x && x.invoke && x.onOutput && x.offOutput && x.input);
129 | };
130 |
131 | old = exports.stream;
132 | stream.noConflict = function noConflict() {
133 | exports.stream = old;
134 | return stream;
135 | };
136 | exports.stream = stream;
137 |
138 | _ = {
139 | breaker: {},
140 | arrayProto: Array.prototype,
141 | objProto: Object.prototype,
142 | isArray: Array.isArray || function isArray(obj) {
143 | return _.objProto.toString.call(obj) === '[object Array]';
144 | },
145 | isFunction: function isFunction(obj) {
146 | return _.objProto.toString.call(obj) === '[object Function]';
147 | },
148 | isUndefined: function isUndefined(obj) {
149 | return obj === void 0;
150 | },
151 | argumentsToArray: function argumentsToArray(args) {
152 | return _.arrayProto.slice.call(args);
153 | },
154 | applyArgsToFn: function applyArgsToFn(fn, args) {
155 | try {
156 | fn.apply(window, args);
157 | } catch (e) {
158 | if (console && console.exception) {
159 | console.exception(e);
160 | }
161 | }
162 | },
163 | each: function each(obj, iterator, context) {
164 | var i, l, key;
165 | if (obj === null) {
166 | return;
167 | }
168 | if (_.arrayProto.forEach && obj.forEach === _.arrayProto.forEach) {
169 | obj.forEach(iterator, context);
170 | } else if (obj.length === +obj.length) {
171 | for (i = 0, l = obj.length; i < l; i += 1) {
172 | if (obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj) === _.breaker) {
173 | return;
174 | }
175 | }
176 | } else {
177 | for (key in obj) {
178 | if (obj.hasOwnProperty(key)) {
179 | if (iterator.call(context, obj[key], key, obj) === _.breaker) {
180 | return;
181 | }
182 | }
183 | }
184 | }
185 | },
186 | reject: function reject(obj, iterator, context) {
187 | var results = [];
188 | if (obj === null) {
189 | return results;
190 | }
191 | _.each(obj, function exclude(value, index, list) {
192 | if (!iterator.call(context, value, index, list)) {
193 | results[results.length] = value;
194 | }
195 | });
196 | return results;
197 | }
198 | };
199 |
200 | }));
201 |
--------------------------------------------------------------------------------
/lib/rspectacles/app/public/js/mustache.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * mustache.js - Logic-less {{mustache}} templates with JavaScript
3 | * http://github.com/janl/mustache.js
4 | */
5 |
6 | /*global define: false*/
7 |
8 | (function (root, factory) {
9 | if (typeof exports === "object" && exports) {
10 | factory(exports); // CommonJS
11 | } else {
12 | var mustache = {};
13 | factory(mustache);
14 | if (typeof define === "function" && define.amd) {
15 | define(mustache); // AMD
16 | } else {
17 | root.Mustache = mustache; //