summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Beauchamp <daniel.beauchamp@shopify.com>2014-01-12 23:17:39 -0800
committerDaniel Beauchamp <daniel.beauchamp@shopify.com>2014-01-12 23:17:39 -0800
commit9e8dfe7d4290f0150fdef10230cd0ca7a75c9755 (patch)
treeccd82c459d988ac2846db6178710b47113b377a4
parent5b045724acd44e691552c0fb8f86b61aa2e0cd06 (diff)
parentc49b9bc5d47fe02d26836dee5034fe28490f0ebd (diff)
Merge pull request #296 from Shopify/updating_gems_and_refactoring
Updating gems and refactoring
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml3
-rwxr-xr-xbin/dashing116
-rw-r--r--dashing.gemspec29
-rw-r--r--lib/dashing.rb169
-rw-r--r--lib/dashing/app.rb167
-rw-r--r--lib/dashing/cli.rb109
-rw-r--r--lib/dashing/downloader.rb18
-rw-r--r--test/app_test.rb126
-rw-r--r--test/cli_test.rb176
-rw-r--r--test/downloader_test.rb26
-rw-r--r--test/test_helper.rb35
12 files changed, 601 insertions, 377 deletions
diff --git a/.gitignore b/.gitignore
index 2cc7498..2d12c6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
*DS_STORE
/Gemfile.lock
-*.gem \ No newline at end of file
+*.gem
+coverage/
+.ruby-version
diff --git a/.travis.yml b/.travis.yml
index cbb92e0..0a28e78 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,7 @@
language: ruby
rvm:
+ - 2.1.0
- 2.0.0
- 1.9.3
-script: "rake test" \ No newline at end of file
+script: "rake test"
diff --git a/bin/dashing b/bin/dashing
index 9d97700..97a7af5 100755
--- a/bin/dashing
+++ b/bin/dashing
@@ -1,112 +1,8 @@
#!/usr/bin/env ruby
+require "pathname"
+bin_file = Pathname.new(__FILE__).realpath
+$:.unshift File.expand_path("../../lib", bin_file)
-require 'thor'
-require 'net/http'
-require 'json'
-require 'open-uri'
-
-class MockScheduler
- def method_missing(*args)
- yield
- end
-end
-
-SCHEDULER = MockScheduler.new
-
-module Dashing
-
- class CLI < Thor
- include Thor::Actions
-
- class << self
- attr_accessor :auth_token
-
- def hyphenate(str)
- return str.downcase if str =~ /^[A-Z-]+$/
- str.gsub('_', '-').gsub(/\B[A-Z]/, '-\&').squeeze('-').downcase
- end
- end
-
- attr_accessor :name
-
- no_tasks do
- ['widget', 'dashboard', 'job'].each do |type|
- define_method "generate_#{type}" do |name|
- @name = Thor::Util.snake_case(name)
- directory type.to_sym, File.join("#{type}s")
- end
- end
- end
-
- def self.source_root
- File.expand_path('../../templates', __FILE__)
- end
-
- desc "new PROJECT_NAME", "Sets up ALL THE THINGS needed for your dashboard project."
- def new(name)
- @name = Thor::Util.snake_case(name)
- directory :project, @name
- end
-
- desc "generate (widget/dashboard/job) NAME", "Creates a new widget, dashboard, or job."
- def generate(type, name)
- send("generate_#{type}".to_sym, name)
- rescue NoMethodError => e
- puts "Invalid generator. Either use widget, dashboard, or job"
- end
- map "g" => :generate
-
- desc "install GIST_ID", "Installs a new widget from a gist."
- def install(gist_id)
- public_url = "https://gist.github.com/#{gist_id}"
- gist = JSON.parse(open("https://api.github.com/gists/#{gist_id}").read)
-
- gist['files'].each do |filename, contents|
- if filename.end_with?(".rb")
- create_file File.join(Dir.pwd, 'jobs', filename), contents['content']
- elsif filename.end_with?(".coffee", ".html", ".scss")
- widget_name = File.basename(filename, '.*')
- create_file File.join(Dir.pwd, 'widgets', widget_name, filename), contents['content']
- end
- end
-
- print set_color("Don't forget to edit the ", :yellow)
- print set_color("Gemfile ", :yellow, :bold)
- print set_color("and run ", :yellow)
- print set_color("bundle install ", :yellow, :bold)
- say set_color("if needed. More information for this widget can be found at #{public_url}", :yellow)
-
- rescue OpenURI::HTTPError => e
- say set_color("Could not find gist at #{public_url}"), :red
- end
- map "i" => :install
-
- desc "start", "Starts the server in style!"
- method_option :job_path, :desc => "Specify the directory where jobs are stored"
- def start(*args)
- port_option = args.include?('-p')? '' : ' -p 3030'
- args = args.join(" ")
- command = "bundle exec thin -R config.ru start #{port_option} #{args}"
- command.prepend "export JOB_PATH=#{options[:job_path]}; " if options[:job_path]
- system(command)
- end
- map "s" => :start
-
- desc "stop", "Stops the thin server"
- def stop
- command = "bundle exec thin stop"
- system(command)
- end
-
- desc "job JOB_NAME AUTH_TOKEN(optional)", "Runs the specified job. Make sure to supply your auth token if you have one set."
- def job(name, auth_token = "")
- Dir[File.join(Dir.pwd, 'lib/**/*.rb')].each {|file| require file }
- self.class.auth_token = auth_token
- f = File.join(Dir.pwd, "jobs", "#{name}.rb")
- require f
- end
-
- end
-end
-
-Dashing::CLI.start
+require 'dashing'
+Dashing::CLI.source_root(File.expand_path('../../templates', bin_file))
+Dashing::CLI.start(ARGV)
diff --git a/dashing.gemspec b/dashing.gemspec
index 61c0b3b..8ef27f8 100644
--- a/dashing.gemspec
+++ b/dashing.gemspec
@@ -11,20 +11,25 @@ Gem::Specification.new do |s|
s.description = "This framework lets you build & easily layout dashboards with your own custom widgets. Use it to make a status boards for your ops team, or use it to track signups, conversion rates, or whatever else metrics you'd like to see in one spot. Included with the framework are ready-made widgets for you to use or customize. All of this code was extracted out of a project at Shopify that displays dashboards on TVs around the office."
s.author = "Daniel Beauchamp"
s.email = 'daniel.beauchamp@shopify.com'
- s.files = ["lib/Dashing.rb"]
s.homepage = 'http://shopify.github.com/dashing'
s.files = Dir['README.md', 'javascripts/**/*', 'templates/**/*','templates/**/.[a-z]*', 'lib/**/*']
- s.add_dependency('sass')
- s.add_dependency('coffee-script', '>=1.6.2')
- s.add_dependency('execjs', '>=2.0.0')
- s.add_dependency('sinatra')
- s.add_dependency('sinatra-contrib')
- s.add_dependency('thin')
- s.add_dependency('rufus-scheduler', '~> 2.0')
- s.add_dependency('thor')
- s.add_dependency('sprockets')
- s.add_dependency('rack')
+ s.add_dependency('sass', '~> 3.2.12')
+ s.add_dependency('coffee-script', '~> 2.2.0')
+ s.add_dependency('execjs', '~> 2.0.2')
+ s.add_dependency('sinatra', '~> 1.4.4')
+ s.add_dependency('sinatra-contrib', '~> 1.4.2')
+ s.add_dependency('thin', '~> 1.6.1')
+ s.add_dependency('rufus-scheduler', '~> 3.0.3')
+ s.add_dependency('thor', '~> 0.18.1')
+ s.add_dependency('sprockets', '~> 2.10.1')
+ s.add_dependency('rack', '~> 1.5.2')
-end \ No newline at end of file
+ s.add_development_dependency('rake', '~> 10.1.0')
+ s.add_development_dependency('haml', '~> 4.0.4')
+ s.add_development_dependency('minitest', '~> 5.2.0')
+ s.add_development_dependency('mocha', '~> 0.14.0')
+ s.add_development_dependency('fakeweb', '~> 1.3.0')
+ s.add_development_dependency('simplecov', '~> 0.8.2')
+end
diff --git a/lib/dashing.rb b/lib/dashing.rb
index a8e9f33..855aa36 100644
--- a/lib/dashing.rb
+++ b/lib/dashing.rb
@@ -1,167 +1,6 @@
-require 'sinatra'
-require 'sprockets'
-require 'sinatra/content_for'
-require 'rufus/scheduler'
-require 'coffee-script'
-require 'sass'
-require 'json'
-require 'yaml'
+require 'dashing/cli'
+require 'dashing/downloader'
+require 'dashing/app'
-SCHEDULER = Rufus::Scheduler.start_new
-
-set :root, Dir.pwd
-
-set :sprockets, Sprockets::Environment.new(settings.root)
-set :assets_prefix, '/assets'
-set :digest_assets, false
-['assets/javascripts', 'assets/stylesheets', 'assets/fonts', 'assets/images', 'widgets', File.expand_path('../../javascripts', __FILE__)]. each do |path|
- settings.sprockets.append_path path
-end
-
-set server: 'thin', connections: [], history_file: 'history.yml'
-
-# Persist history in tmp file at exit
-at_exit do
- File.open(settings.history_file, 'w') do |f|
- f.puts settings.history.to_yaml
- end
-end
-
-if File.exists?(settings.history_file)
- set history: YAML.load_file(settings.history_file)
-else
- set history: {}
-end
-
-set :public_folder, File.join(settings.root, 'public')
-set :views, File.join(settings.root, 'dashboards')
-set :default_dashboard, nil
-set :auth_token, nil
-
-helpers Sinatra::ContentFor
-helpers do
- def protected!
- # override with auth logic
- end
-end
-
-get '/events', provides: 'text/event-stream' do
- protected!
- response.headers['X-Accel-Buffering'] = 'no' # Disable buffering for nginx
- stream :keep_open do |out|
- settings.connections << out
- out << latest_events
- out.callback { settings.connections.delete(out) }
- end
-end
-
-get '/' do
- protected!
- begin
- redirect "/" + (settings.default_dashboard || first_dashboard).to_s
- rescue NoMethodError => e
- raise Exception.new("There are no dashboards in your dashboard directory.")
- end
-end
-
-get '/:dashboard' do
- protected!
- tilt_html_engines.each do |suffix, _|
- file = File.join(settings.views, "#{params[:dashboard]}.#{suffix}")
- return render(suffix.to_sym, params[:dashboard].to_sym) if File.exist? file
- end
-
- halt 404
+module Dashing
end
-
-get '/views/:widget?.html' do
- protected!
- tilt_html_engines.each do |suffix, engines|
- file = File.join(settings.root, "widgets", params[:widget], "#{params[:widget]}.#{suffix}")
- return engines.first.new(file).render if File.exist? file
- end
-end
-
-post '/dashboards/:id' do
- request.body.rewind
- body = JSON.parse(request.body.read)
- body['dashboard'] ||= params['id']
- auth_token = body.delete("auth_token")
- if !settings.auth_token || settings.auth_token == auth_token
- send_event(params['id'], body, 'dashboards')
- 204 # response without entity body
- else
- status 401
- "Invalid API key\n"
- end
-end
-
-post '/widgets/:id' do
- request.body.rewind
- body = JSON.parse(request.body.read)
- auth_token = body.delete("auth_token")
- if !settings.auth_token || settings.auth_token == auth_token
- send_event(params['id'], body)
- 204 # response without entity body
- else
- status 401
- "Invalid API key\n"
- end
-end
-
-not_found do
- send_file File.join(settings.public_folder, '404.html')
-end
-
-def development?
- ENV['RACK_ENV'] == 'development'
-end
-
-def production?
- ENV['RACK_ENV'] == 'production'
-end
-
-def send_event(id, body, target=nil)
- body[:id] = id
- body[:updatedAt] ||= Time.now.to_i
- event = format_event(body.to_json, target)
- Sinatra::Application.settings.history[id] = event unless target == 'dashboards'
- Sinatra::Application.settings.connections.each { |out| out << event }
-end
-
-def format_event(body, name=nil)
- str = ""
- str << "event: #{name}\n" if name
- str << "data: #{body}\n\n"
-end
-
-def latest_events
- settings.history.inject("") do |str, (id, body)|
- str << body
- end
-end
-
-def first_dashboard
- files = Dir[File.join(settings.views, '*')].collect { |f| File.basename(f, '.*') }
- files -= ['layout']
- files.sort.first
-end
-
-def tilt_html_engines
- Tilt.mappings.select do |_, engines|
- default_mime_type = engines.first.default_mime_type
- default_mime_type.nil? || default_mime_type == 'text/html'
- end
-end
-
-settings_file = File.join(settings.root, 'config/settings.rb')
-if (File.exists?(settings_file))
- require settings_file
-end
-
-Dir[File.join(settings.root, 'lib', '**', '*.rb')].each {|file| require file }
-{}.to_json # Forces your json codec to initialize (in the event that it is lazily loaded). Does this before job threads start.
-
-job_path = ENV["JOB_PATH"] || 'jobs'
-files = Dir[File.join(settings.root, job_path, '**', '/*.rb')]
-files.each { |job| require(job) }
diff --git a/lib/dashing/app.rb b/lib/dashing/app.rb
new file mode 100644
index 0000000..033849c
--- /dev/null
+++ b/lib/dashing/app.rb
@@ -0,0 +1,167 @@
+require 'sinatra'
+require 'sprockets'
+require 'sinatra/content_for'
+require 'rufus/scheduler'
+require 'coffee-script'
+require 'sass'
+require 'json'
+require 'yaml'
+
+SCHEDULER = Rufus::Scheduler.new
+
+def development?
+ ENV['RACK_ENV'] == 'development'
+end
+
+def production?
+ ENV['RACK_ENV'] == 'production'
+end
+
+helpers Sinatra::ContentFor
+helpers do
+ def protected!
+ # override with auth logic
+ end
+end
+
+set :root, Dir.pwd
+set :sprockets, Sprockets::Environment.new(settings.root)
+set :assets_prefix, '/assets'
+set :digest_assets, false
+set server: 'thin', connections: [], history_file: 'history.yml'
+set :public_folder, File.join(settings.root, 'public')
+set :views, File.join(settings.root, 'dashboards')
+set :default_dashboard, nil
+set :auth_token, nil
+
+if File.exists?(settings.history_file)
+ set history: YAML.load_file(settings.history_file)
+else
+ set history: {}
+end
+
+%w(javascripts stylesheets fonts images).each do |path|
+ settings.sprockets.append_path("assets/#{path}")
+end
+
+['widgets', File.expand_path('../../../javascripts', __FILE__)]. each do |path|
+ settings.sprockets.append_path(path)
+end
+
+not_found do
+ send_file File.join(settings.public_folder, '404.html')
+end
+
+at_exit do
+ File.write(settings.history_file, settings.history.to_yaml)
+end
+
+get '/' do
+ protected!
+ dashboard = settings.default_dashboard || first_dashboard
+ raise Exception.new('There are no dashboards available') if not dashboard
+
+ redirect "/" + dashboard
+end
+
+get '/events', provides: 'text/event-stream' do
+ protected!
+ response.headers['X-Accel-Buffering'] = 'no' # Disable buffering for nginx
+ stream :keep_open do |out|
+ settings.connections << out
+ out << latest_events
+ out.callback { settings.connections.delete(out) }
+ end
+end
+
+get '/:dashboard' do
+ protected!
+ tilt_html_engines.each do |suffix, _|
+ file = File.join(settings.views, "#{params[:dashboard]}.#{suffix}")
+ return render(suffix.to_sym, params[:dashboard].to_sym) if File.exist? file
+ end
+
+ halt 404
+end
+
+post '/dashboards/:id' do
+ request.body.rewind
+ body = JSON.parse(request.body.read)
+ body['dashboard'] ||= params['id']
+ auth_token = body.delete("auth_token")
+ if !settings.auth_token || settings.auth_token == auth_token
+ send_event(params['id'], body, 'dashboards')
+ 204 # response without entity body
+ else
+ status 401
+ "Invalid API key\n"
+ end
+end
+
+post '/widgets/:id' do
+ request.body.rewind
+ body = JSON.parse(request.body.read)
+ auth_token = body.delete("auth_token")
+ if !settings.auth_token || settings.auth_token == auth_token
+ send_event(params['id'], body)
+ 204 # response without entity body
+ else
+ status 401
+ "Invalid API key\n"
+ end
+end
+
+get '/views/:widget?.html' do
+ protected!
+ tilt_html_engines.each do |suffix, engines|
+ file = File.join(settings.root, "widgets", params[:widget], "#{params[:widget]}.#{suffix}")
+ return engines.first.new(file).render if File.exist? file
+ end
+end
+
+def send_event(id, body, target=nil)
+ body[:id] = id
+ body[:updatedAt] ||= Time.now.to_i
+ event = format_event(body.to_json, target)
+ Sinatra::Application.settings.history[id] = event unless target == 'dashboards'
+ Sinatra::Application.settings.connections.each { |out| out << event }
+end
+
+def format_event(body, name=nil)
+ str = ""
+ str << "event: #{name}\n" if name
+ str << "data: #{body}\n\n"
+end
+
+def latest_events
+ settings.history.inject("") do |str, (id, body)|
+ str << body
+ end
+end
+
+def first_dashboard
+ files = Dir[File.join(settings.views, '*')].collect { |f| File.basename(f, '.*') }
+ files -= ['layout']
+ files.sort.first
+end
+
+def tilt_html_engines
+ Tilt.mappings.select do |_, engines|
+ default_mime_type = engines.first.default_mime_type
+ default_mime_type.nil? || default_mime_type == 'text/html'
+ end
+end
+
+def require_glob(relative_glob)
+ Dir[File.join(settings.root, relative_glob)].each do |file|
+ require file
+ end
+end
+
+settings_file = File.join(settings.root, 'config/settings.rb')
+require settings_file if File.exists?(settings_file)
+
+{}.to_json # Forces your json codec to initialize (in the event that it is lazily loaded). Does this before job threads start.
+job_path = ENV["JOB_PATH"] || 'jobs'
+require_glob(File.join('lib', '**', '*.rb'))
+require_glob(File.join(job_path, '**', '*.rb'))
diff --git a/lib/dashing/cli.rb b/lib/dashing/cli.rb
new file mode 100644
index 0000000..27d8f6b
--- /dev/null
+++ b/lib/dashing/cli.rb
@@ -0,0 +1,109 @@
+require 'thor'
+require 'open-uri'
+
+module Dashing
+ class CLI < Thor
+ include Thor::Actions
+
+ attr_reader :name
+
+ class << self
+ attr_accessor :auth_token
+
+ def hyphenate(str)
+ return str.downcase if str =~ /^[A-Z-]+$/
+ str.gsub('_', '-').gsub(/\B[A-Z]/, '-\&').squeeze('-').downcase
+ end
+ end
+
+ no_tasks do
+ %w(widget dashboard job).each do |type|
+ define_method "generate_#{type}" do |name|
+ @name = Thor::Util.snake_case(name)
+ directory(type.to_sym, "#{type}s")
+ end
+ end
+ end
+
+ desc "new PROJECT_NAME", "Sets up ALL THE THINGS needed for your dashboard project."
+ def new(name)
+ @name = Thor::Util.snake_case(name)
+ directory(:project, @name)
+ end
+
+ desc "generate (widget/dashboard/job) NAME", "Creates a new widget, dashboard, or job."
+ def generate(type, name)
+ public_send("generate_#{type}".to_sym, name)
+ rescue NoMethodError => e
+ puts "Invalid generator. Either use widget, dashboard, or job"
+ end
+
+ desc "install GIST_ID", "Installs a new widget from a gist."
+ def install(gist_id)
+ gist = Downloader.get_gist(gist_id)
+ public_url = "https://gist.github.com/#{gist_id}"
+
+ install_widget_from_gist(gist)
+
+ print set_color("Don't forget to edit the ", :yellow)
+ print set_color("Gemfile ", :yellow, :bold)
+ print set_color("and run ", :yellow)
+ print set_color("bundle install ", :yellow, :bold)
+ say set_color("if needed. More information for this widget can be found at #{public_url}", :yellow)
+ rescue OpenURI::HTTPError => http_error
+ say set_color("Could not find gist at #{public_url}"), :red
+ end
+
+ desc "start", "Starts the server in style!"
+ method_option :job_path, :desc => "Specify the directory where jobs are stored"
+ def start(*args)
+ port_option = args.include?('-p') ? '' : ' -p 3030'
+ args = args.join(' ')
+ command = "bundle exec thin -R config.ru start#{port_option} #{args}"
+ command.prepend "export JOB_PATH=#{options[:job_path]}; " if options[:job_path]
+ run_command(command)
+ end
+
+ desc "stop", "Stops the thin server"
+ def stop
+ command = "bundle exec thin stop"
+ run_command(command)
+ end
+
+ desc "job JOB_NAME AUTH_TOKEN(optional)", "Runs the specified job. Make sure to supply your auth token if you have one set."
+ def job(name, auth_token = "")
+ Dir[File.join(Dir.pwd, 'lib/**/*.rb')].each {|file| require_file(file) }
+ self.class.auth_token = auth_token
+ f = File.join(Dir.pwd, "jobs", "#{name}.rb")
+ require_file(f)
+ end
+
+ # map some commands
+ map 'g' => :generate
+ map 'i' => :install
+ map 's' => :start
+
+ private
+
+ def run_command(command)
+ system(command)
+ end
+
+ def install_widget_from_gist(gist)
+ gist['files'].each do |file, details|
+ if file =~ /\.(html|coffee|scss)\z/
+ widget_name = File.basename(file, '.*')
+ new_path = File.join(Dir.pwd, 'widgets', widget_name, file)
+ create_file(new_path, details['content'])
+ elsif file.end_with?('.rb')
+ new_path = File.join(Dir.pwd, 'jobs', file)
+ create_file(new_path, details['content'])
+ end
+ end
+ end
+
+ def require_file(file)
+ require file
+ end
+ end
+end
diff --git a/lib/dashing/downloader.rb b/lib/dashing/downloader.rb
new file mode 100644
index 0000000..140e862
--- /dev/null
+++ b/lib/dashing/downloader.rb
@@ -0,0 +1,18 @@
+require 'net/http'
+require 'open-uri'
+require 'json'
+
+module Dashing
+ module Downloader
+ extend self
+
+ def get_gist(gist_id)
+ get_json("https://api.github.com/gists/#{gist_id}")
+ end
+
+ def get_json(url)
+ response = open(url).read
+ JSON.parse(response)
+ end
+ end
+end
diff --git a/test/app_test.rb b/test/app_test.rb
index 0032165..4101e91 100644
--- a/test/app_test.rb
+++ b/test/app_test.rb
@@ -1,13 +1,48 @@
require 'test_helper'
-require File.expand_path('../../lib/dashing', __FILE__)
-Sinatra::Application.settings.history_file = File.join(Dir.tmpdir, 'history.yml')
+require 'haml'
class AppTest < Dashing::Test
def setup
@connection = []
- Sinatra::Application.settings.connections = [@connection]
- Sinatra::Application.settings.auth_token = nil
- Sinatra::Application.settings.default_dashboard = nil
+ app.settings.connections = [@connection]
+ app.settings.auth_token = nil
+ app.settings.default_dashboard = nil
+ app.settings.history_file = File.join(Dir.tmpdir, 'history.yml')
+ end
+
+ def test_redirect_to_first_dashboard
+ with_generated_project do
+ get '/'
+ assert_equal 302, last_response.status
+ assert_equal 'http://example.org/sample', last_response.location
+ end
+ end
+
+ def test_redirect_to_first_dashboard_without_erb
+ with_generated_project do |dir|
+ FileUtils.touch(File.join(dir, "dashboards/htmltest.html"))
+ get '/'
+ assert_equal 302, last_response.status
+ assert_equal 'http://example.org/htmltest', last_response.location
+ end
+ end
+
+ def test_redirect_to_default_dashboard
+ with_generated_project do
+ app.settings.default_dashboard = 'test1'
+ get '/'
+ assert_equal 302, last_response.status
+ assert_equal 'http://example.org/test1', last_response.location
+ end
+ end
+
+ def test_errors_out_when_no_dashboards_available
+ with_generated_project do
+ app.settings.views = File.join(app.settings.root, 'lib')
+
+ get '/'
+ assert_equal 500, last_response.status
+ end
end
def test_post_widgets_without_auth_token
@@ -22,13 +57,13 @@ class AppTest < Dashing::Test
end
def test_post_widgets_with_invalid_auth_token
- Sinatra::Application.settings.auth_token = 'sekrit'
+ app.settings.auth_token = 'sekrit'
post '/widgets/some_widget', JSON.generate({value: 9})
assert_equal 401, last_response.status
end
def test_post_widgets_with_valid_auth_token
- Sinatra::Application.settings.auth_token = 'sekrit'
+ app.settings.auth_token = 'sekrit'
post '/widgets/some_widget', JSON.generate({value: 9, auth_token: 'sekrit'})
assert_equal 204, last_response.status
end
@@ -52,71 +87,39 @@ class AppTest < Dashing::Test
assert_equal 'reload', parse_data(@connection[0])['event']
end
- def test_redirect_to_default_dashboard
- with_generated_project do
- Sinatra::Application.settings.default_dashboard = 'test1'
- get '/'
- assert_equal 302, last_response.status
- assert_equal 'http://example.org/test1', last_response.location
- end
- end
-
- def test_redirect_to_first_dashboard
- with_generated_project do
- get '/'
- assert_equal 302, last_response.status
- assert_equal 'http://example.org/sample', last_response.location
- end
- end
-
- def test_redirect_to_first_dashboard_without_erb
- with_generated_project do |dir|
- FileUtils.touch(File.join(dir, "dashboards/htmltest.html"))
- get '/'
- assert_equal 302, last_response.status
- assert_equal 'http://example.org/htmltest', last_response.location
- end
- end
-
def test_get_dashboard
with_generated_project do
get '/sampletv'
assert_equal 200, last_response.status
- assert_include last_response.body, 'class="gridster"'
- assert_include last_response.body, "DOCTYPE"
+ assert_includes last_response.body, 'class="gridster"'
+ assert_includes last_response.body, "DOCTYPE"
end
end
def test_page_title_set_correctly
with_generated_project do
get '/sampletv'
- assert_include last_response.body, '<title>1080p dashboard</title>'
+ assert_includes last_response.body, '<title>1080p dashboard</title>'
end
end
- begin
- require 'haml'
-
- def test_get_haml_dashboard
- with_generated_project do |dir|
- File.write(File.join(dir, 'dashboards/hamltest.haml'), '.gridster')
- get '/hamltest'
- assert_equal 200, last_response.status
- assert_include last_response.body, "class='gridster'"
- end
+ def test_get_haml_dashboard
+ with_generated_project do |dir|
+ File.write(File.join(dir, 'dashboards/hamltest.haml'), '.gridster')
+ get '/hamltest'
+ assert_equal 200, last_response.status
+ assert_includes last_response.body, "class='gridster'"
end
+ end
- def test_get_haml_widget
- with_generated_project do |dir|
- File.write(File.join(dir, 'widgets/clock/clock.haml'), '%h1 haml')
- File.unlink(File.join(dir, 'widgets/clock/clock.html'))
- get '/views/clock.html'
- assert_equal 200, last_response.status
- assert_include last_response.body, '<h1>haml</h1>'
- end
+ def test_get_haml_widget
+ with_generated_project do |dir|
+ File.write(File.join(dir, 'widgets/clock/clock.haml'), '%h1 haml')
+ File.unlink(File.join(dir, 'widgets/clock/clock.html'))
+ get '/views/clock.html'
+ assert_equal 200, last_response.status
+ assert_includes last_response.body, '<h1>haml</h1>'
end
- rescue LoadError
- puts "[skipping haml tests because haml isn't installed]"
end
def test_get_nonexistent_dashboard
@@ -130,18 +133,21 @@ class AppTest < Dashing::Test
with_generated_project do
get '/views/meter.html'
assert_equal 200, last_response.status
- assert_include last_response.body, 'class="meter"'
+ assert_includes last_response.body, 'class="meter"'
end
end
def with_generated_project
+ source_path = File.expand_path('../../templates', __FILE__)
+
temp do |dir|
cli = Dashing::CLI.new
+ cli.stubs(:source_paths).returns([source_path])
silent { cli.new 'new_project' }
- Sinatra::Application.settings.views = File.join(dir, 'new_project/dashboards')
- Sinatra::Application.settings.root = File.join(dir, 'new_project')
- yield Sinatra::Application.settings.root
+ app.settings.views = File.join(dir, 'new_project/dashboards')
+ app.settings.root = File.join(dir, 'new_project')
+ yield app.settings.root
end
end
diff --git a/test/cli_test.rb b/test/cli_test.rb
index 6c43e2c..567827e 100644
--- a/test/cli_test.rb
+++ b/test/cli_test.rb
@@ -1,28 +1,168 @@
require 'test_helper'
-silent{ load 'bin/dashing' }
-module Thor::Actions
- def source_paths
- [File.join(File.expand_path(File.dirname(__FILE__)), '../templates')]
+class CLITest < Dashing::Test
+ def setup
+ @cli = Dashing::CLI.new
+ end
+
+ def test_new_task_creates_project_directory
+ app_name = 'custom_dashboard'
+ @cli.stubs(:directory).with(:project, app_name).once
+ @cli.new(app_name)
end
-end
-class CliTest < Dashing::Test
+ def test_generate_task_delegates_to_type
+ types = %w(widget dashboard job)
- def test_project_directory_created
- temp do |dir|
- cli = Dashing::CLI.new
- silent{ cli.new 'Dashboard' }
- assert Dir.exist?(File.join(dir,'dashboard')), 'Dashing directory was not created.'
+ types.each do |type|
+ @cli.stubs(:public_send).with("generate_#{type}".to_sym, 'name').once
+ @cli.generate(type, 'name')
end
end
- def test_hyphenate
- assert_equal 'power', Dashing::CLI.hyphenate('Power')
- assert_equal 'power', Dashing::CLI.hyphenate('POWER')
- assert_equal 'power-rangers', Dashing::CLI.hyphenate('PowerRangers')
- assert_equal 'power-ranger', Dashing::CLI.hyphenate('Power_ranger')
- assert_equal 'super-power-rangers', Dashing::CLI.hyphenate('SuperPowerRangers')
+ def test_generate_task_warns_when_generator_is_not_defined
+ output, _ = capture_io do
+ @cli.generate('wtf', 'name')
+ end
+
+ assert_includes output, 'Invalid generator'
+ end
+
+ def test_generate_widget_creates_a_new_widget
+ @cli.stubs(:directory).with(:widget, 'widgets').once
+ @cli.generate_widget('WidgetName')
+ assert_equal 'widget_name', @cli.name
+ end
+
+ def test_generate_dashboard_creates_a_new_dashboard
+ @cli.stubs(:directory).with(:dashboard, 'dashboards').once
+ @cli.generate_dashboard('DashBoardName')
+ assert_equal 'dash_board_name', @cli.name
+ end
+
+ def test_generate_job_creates_a_new_job
+ @cli.stubs(:directory).with(:job, 'jobs').once
+ @cli.generate_job('MyCustomJob')
+ assert_equal 'my_custom_job', @cli.name
+ end
+
+ def test_install_task_requests_gist_from_downloader
+ return_value = { 'files' => [] }
+ Dashing::Downloader.stubs(:get_gist).with(123).returns(return_value).once
+
+ capture_io { @cli.install(123) }
+ end
+
+ def test_install_task_calls_create_file_for_each_valid_file_in_gist
+ json_response = <<-JSON
+ {
+ "files": {
+ "ruby_job.rb": { "content": "some job content" },
+ "num.html": { "content": "some html content" },
+ "num.scss": { "content": "some sass content" },
+ "num.coffee": { "content": "some coffee content" }
+ }
+ }
+ JSON
+
+ Dir.stubs(:pwd).returns('')
+
+ Dashing::Downloader.stubs(:get_gist).returns(JSON.parse(json_response))
+ @cli.stubs(:create_file).with('/jobs/ruby_job.rb', 'some job content').once
+ @cli.stubs(:create_file).with('/widgets/num/num.html', 'some html content').once
+ @cli.stubs(:create_file).with('/widgets/num/num.scss', 'some sass content').once
+ @cli.stubs(:create_file).with('/widgets/num/num.coffee', 'some coffee content').once
+
+ capture_io { @cli.install(123) }
+ end
+
+ def test_install_task_ignores_invalid_files
+ json_response = <<-JSON
+ {
+ "files": {
+ "ruby_job.js": { "content": "some job content" },
+ "num.css": { "content": "some sass content" }
+ }
+ }
+ JSON
+
+ Dashing::Downloader.stubs(:get_gist).returns(JSON.parse(json_response))
+ @cli.stubs(:create_file).never
+
+ capture_io { @cli.install(123) }
+ end
+
+ def test_install_task_warns_when_gist_not_found
+ error = OpenURI::HTTPError.new('error', mock())
+ Dashing::Downloader.stubs(:get_gist).raises(error)
+
+ output, _ = capture_io { @cli.install(123) }
+
+ assert_includes output, 'Could not find gist at '
+ end
+
+ def test_start_task_starts_thin_with_default_port
+ command = 'bundle exec thin -R config.ru start -p 3030 '
+ @cli.stubs(:run_command).with(command).once
+ @cli.start
end
-end \ No newline at end of file
+ def test_start_task_starts_thin_with_specified_port
+ command = 'bundle exec thin -R config.ru start -p 2020'
+ @cli.stubs(:run_command).with(command).once
+ @cli.start('-p', '2020')
+ end
+
+ def test_start_task_supports_job_path_option
+ commands = [
+ 'export JOB_PATH=other_spot; ',
+ 'bundle exec thin -R config.ru start -p 3030 '
+ ]
+
+ @cli.stubs(:options).returns(job_path: 'other_spot')
+ @cli.stubs(:run_command).with(commands.join('')).once
+ @cli.start
+ end
+
+ def test_stop_task_stops_thin_server
+ @cli.stubs(:run_command).with('bundle exec thin stop')
+ @cli.stop
+ end
+
+ def test_job_task_requires_job_file
+ Dir.stubs(:pwd).returns('')
+ @cli.stubs(:require_file).with('/jobs/special_job.rb').once
+
+ @cli.job('special_job')
+ end
+
+ def test_job_task_requires_every_ruby_file_in_lib
+ Dir.stubs(:pwd).returns('')
+ Dir.stubs(:[]).returns(['lib/dashing/cli.rb', 'lib/dashing.rb'])
+ @cli.stubs(:require_file).times(3)
+
+ @cli.job('special_job')
+ end
+
+ def test_job_sets_auth_token
+ @cli.class.stubs(:auth_token=).with('my_token').once
+ @cli.stubs(:require_file)
+
+ @cli.job('my_job', 'my_token')
+ end
+
+ def test_hyphenate_lowers_and_hyphenates_inputs
+ assertion_map = {
+ 'Power' => 'power',
+ 'POWER' => 'power',
+ 'PowerRangers' => 'power-rangers',
+ 'Power_ranger' => 'power-ranger',
+ 'SuperPowerRangers' => 'super-power-rangers'
+ }
+
+ assertion_map.each do |input, expected|
+ assert_equal expected, Dashing::CLI.hyphenate(input)
+ end
+ end
+
+end
diff --git a/test/downloader_test.rb b/test/downloader_test.rb
new file mode 100644
index 0000000..930ad56
--- /dev/null
+++ b/test/downloader_test.rb
@@ -0,0 +1,26 @@
+require 'test_helper'
+
+class DownloaderTest < Minitest::Test
+
+ def test_get_json_requests_and_parses_content
+ endpoint = 'http://somehost.com/file.json'
+ response = '{ "name": "value" }'
+ FakeWeb.register_uri(:get, endpoint, body: response)
+ JSON.stubs(:parse).with(response).once
+
+ Dashing::Downloader.get_json(endpoint)
+ end
+
+ def test_get_json_raises_on_bad_request
+ FakeWeb.register_uri(:get, 'http://dead-host.com/', status: '404')
+
+ assert_raises(OpenURI::HTTPError) do
+ Dashing::Downloader.get_json('http://dead-host.com/')
+ end
+ end
+
+ def test_load_gist_attempts_to_get_the_gist
+ Dashing::Downloader.stubs(:get_json).once
+ Dashing::Downloader.get_gist(123)
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d2337c5..0b719f5 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,19 +1,28 @@
+require 'simplecov'
+SimpleCov.start do
+ add_filter "/vendor/"
+ add_filter "/test/"
+end
+
require 'rack/test'
require 'stringio'
-require 'test/unit'
require 'tmpdir'
+require 'fakeweb'
+require 'minitest/autorun'
+require 'minitest/pride'
+require 'mocha/setup'
+
+require_relative '../lib/dashing'
+
+FakeWeb.allow_net_connect = false
ENV['RACK_ENV'] = 'test'
WORKING_DIRECTORY = Dir.pwd.freeze
ARGV.clear
-def silent
- _stdout = $stdout
- $stdout = mock = StringIO.new
- begin
- yield
- ensure
- $stdout = _stdout
+def load_quietly(file)
+ Minitest::Test.new(nil).capture_io do
+ load file
end
end
@@ -28,7 +37,13 @@ ensure
end
module Dashing
- class Test < Test::Unit::TestCase
+ class Test < Minitest::Test
include Rack::Test::Methods
+
+ alias_method :silent, :capture_io
+
+ def teardown
+ FileUtils.rm_f('history.yml')
+ end
end
-end \ No newline at end of file
+end