spassky 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ <html>
2
+ <head>
3
+ </head>
4
+ <body>
5
+ <h1>A QUnit Suite</h1>
6
+ <script type="text/javascript" src="qunit.js"></script>
7
+ <script type="text/javascript" src="main.js"></script>
8
+ </body>
9
+ </html>
@@ -2,8 +2,8 @@ Feature: Connection
2
2
  In order to run tests with minimal effort
3
3
  As a web developer
4
4
  I want to connect devices to a constantly running service
5
-
5
+
6
6
  Scenario: Connect a device
7
7
  Given a connected mobile device "ipad"
8
8
  Then it should wait for a test to run
9
- And the word "Idle" should appear on the device
9
+ And the word "Idle" should appear on the device
@@ -2,7 +2,7 @@ Feature: Device Timeout
2
2
  In order for a clean test run
3
3
  As a Developer-in-Test
4
4
  I want to ignore devices that haven't connected recently
5
-
5
+
6
6
  Background: One passing test
7
7
  Given a file named "timed-out.html" with:
8
8
  """
@@ -17,7 +17,7 @@ Feature: Device Timeout
17
17
  </body>
18
18
  </html>
19
19
  """
20
-
20
+
21
21
  Scenario: One device times out
22
22
  Given a connected mobile device "ipad"
23
23
  When the device disconnects
@@ -27,5 +27,5 @@ Feature: Device Timeout
27
27
  TIMED OUT timed-out.html on ipad
28
28
  """
29
29
  And the exit status should be 2
30
-
31
-
30
+
31
+
@@ -0,0 +1,21 @@
1
+ Feature: List Connected Devices
2
+ In order to easily diagnose device connection issues
3
+ As a JavaScript developer
4
+ I want to be able to know what devices are connected
5
+
6
+ Scenario: Two Connected Devices
7
+ Given a Wireless Universal Resource FiLe
8
+ And a connected mobile device "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3"
9
+ And a connected mobile device "Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10"
10
+ When I run "spassky <host> devices" with the server host
11
+ Then the output should contain:
12
+ """
13
+ iPhone (id = apple_iphone_ver1_suba543, mobile_browser = Safari, device_os_version = 1.0)
14
+ iPad (id = apple_ipad_ver1_sub5312110, mobile_browser = Safari, device_os_version = 3.2)
15
+ """
16
+
17
+ #:id => "apple_iphone_ver1_suba543",
18
+ #:mobile_browser => "Safari",
19
+ #:pointing_method => "touchscreen",
20
+ #:model_name => "iPhone",
21
+ #:device_os_version => "1.0",
@@ -32,6 +32,15 @@ Feature: Run HTML Tests
32
32
  </html>
33
33
  """
34
34
 
35
+ Scenario: No connected devices
36
+ Given I have no connected devices
37
+ When I run "spassky <host> passing.html" with the server host
38
+ Then the output should contain:
39
+ """
40
+ There are no connected devices
41
+ """
42
+ And the exit status should be 1
43
+
35
44
  Scenario: One passing test on one device
36
45
  Given a connected mobile device "blackberry"
37
46
  When I run "spassky <host> passing.html" with the server host
@@ -4,42 +4,72 @@ Feature: Run QUnit Tests
4
4
  As a QUnit user
5
5
  I want to test JavaScript code on different web browsers
6
6
 
7
- Background: Two tests, one passes, one fails
8
- Given a file named "qunit_suite/qunit_test/passing.js" with:
7
+ Scenario: One passing suite on one device
8
+ Given a file named "qunit_passing/qunit_test/passing.js" with:
9
9
  """
10
- QUnit.done = function(failed, passed, total, runtime){
11
- assert(true, "pass");
12
-
13
- if (failed.length() > 0) {
10
+ QUnit.done = function(result) {
11
+ if (result.failed > 0) {
14
12
  assert(false, "qunit failed");
15
13
  } else {
16
14
  assert(true, "qunit passed");
17
15
  }
18
16
  };
19
17
 
20
- test("should pass", function() {
21
- ok(true, "it passed!");
18
+ test("it passes", function() {
19
+ ok(true, "it passed");
22
20
  });
23
21
  """
24
- And a file named "qunit_suite/qunit_test/qunit.js" with QUnit.js in it
25
- And a file named "qunit_suite/qunit_test/suite.html" with:
22
+ And a file named "qunit_passing/qunit_test/qunit.js" with QUnit.js in it
23
+ And a file named "qunit_passing/qunit_test/suite.html" with:
26
24
  """
27
25
  <html>
28
- <head>
29
- </head>
26
+ <head></head>
30
27
  <body>
31
- <h1>A QUnit Suite</h1>
32
- <script type="text/javascript" src="qunit.js"></script>
33
- <script type="text/javascript" src="passing.js"></script>
28
+ <h1>A QUnit Suite</h1>
29
+ <script type="text/javascript" src="qunit.js"></script>
30
+ <script type="text/javascript" src="passing.js"></script>
34
31
  </body>
35
32
  </html>
36
33
  """
37
-
38
- Scenario: One passing suite on one device
39
- Given a connected mobile device "blackberry"
40
- When I run "spassky <host> qunit_suite/qunit_test" with the server host
34
+ And a connected mobile device "blackberry"
35
+ When I run "spassky <host> qunit_passing/qunit_test" with the server host
41
36
  Then the output should contain:
42
37
  """
43
38
  PASS qunit_test on blackberry
44
39
  """
45
40
  And the exit status should be 0
41
+
42
+ Scenario: One failing suite on one device
43
+ Given a file named "qunit_failing/qunit_test/failing.js" with:
44
+ """
45
+ QUnit.done = function(result) {
46
+ if (result.failed > 0) {
47
+ assert(false, "qunit failed");
48
+ } else {
49
+ assert(true, "qunit passed");
50
+ }
51
+ };
52
+
53
+ test("it fails", function() {
54
+ ok(false, "it failed");
55
+ });
56
+ """
57
+ And a file named "qunit_failing/qunit_test/qunit.js" with QUnit.js in it
58
+ And a file named "qunit_failing/qunit_test/suite.html" with:
59
+ """
60
+ <html>
61
+ <head></head>
62
+ <body>
63
+ <h1>A QUnit Suite</h1>
64
+ <script type="text/javascript" src="qunit.js"></script>
65
+ <script type="text/javascript" src="failing.js"></script>
66
+ </body>
67
+ </html>
68
+ """
69
+ And a connected mobile device "blackberry"
70
+ When I run "spassky <host> qunit_failing/qunit_test" with the server host
71
+ Then the output should contain:
72
+ """
73
+ FAIL qunit_test on blackberry
74
+ """
75
+ And the exit status should be 1
@@ -4,8 +4,5 @@ Feature: Server
4
4
  I want to be able to type 'spassky-server' to launch a server
5
5
 
6
6
  Scenario: Launch Server
7
- Given I run spassky-server and then exit it
8
- Then I should see the output:
9
- """
10
- Sinatra/1.2.6 has taken the stage on 9191
11
- """
7
+ Given I run spassky-server
8
+ Then it should not crash
@@ -9,12 +9,19 @@ Given /^a connected mobile device "([^"]*)"$/ do |user_agent|
9
9
  @last_user_agent = user_agent
10
10
  end
11
11
 
12
+ Given /^I have no connected devices$/ do
13
+ @uri = URI.parse(current_url)
14
+ end
15
+
12
16
  Given /^a file named "([^"]*)" with ([^\s]*) in it$/ do |file_name, fixture_name|
13
17
  fixture_path = File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "fixtures", fixture_name))
14
18
  fixture_content = File.read(fixture_path)
15
19
  write_file(file_name, fixture_content)
16
20
  end
17
21
 
22
+ Given /^a Wireless Universal Resource FiLe$/ do
23
+ end
24
+
18
25
  When /^I run "([^"]*)" with the server host$/ do |command_line|
19
26
  run_simple(unescape(command_line.gsub('<host>', "http://#{@uri.host}:#{@uri.port}")), false)
20
27
  end
@@ -42,25 +49,12 @@ When /^the device disconnects$/ do
42
49
  end
43
50
  end
44
51
 
45
- Given /^I run spassky\-server and then exit it$/ do
46
- @output = Tempfile.new("output")
47
- process = ChildProcess.build('./bin/spassky-server')
48
- process.io.stdout = @output
49
- process.start
50
- sleep 1
51
- process.stop
52
+ Given /^I run spassky\-server$/ do
53
+ @spassky_server_process = ChildProcess.build('./bin/spassky-server')
54
+ @spassky_server_process.start
52
55
  end
53
56
 
54
- Then /^I should see the output:$/ do |string|
55
- text = ""
56
- sleep_count = 0
57
- while text == ""
58
- @output.rewind
59
- text = @output.read
60
- break if sleep_count = 10
61
- sleep 0.5
62
- sleep_count += 1
63
- end
64
-
65
- text.should include string
57
+ Then /^it should not crash$/ do
58
+ @spassky_server_process.should_not be_crashed
59
+ @spassky_server_process.stop
66
60
  end
@@ -1,3 +1,4 @@
1
+ require 'spassky/client/device_list_retriever'
1
2
  require 'spassky/client/test_runner'
2
3
  require 'spassky/client/pusher'
3
4
  require 'spassky/client/directory_reader'
@@ -5,8 +6,14 @@ require 'spassky/client/directory_reader'
5
6
  module Spassky::Client
6
7
  class Cli
7
8
  def self.run(argv)
8
- writer = argv.include?('--colour') ? ColouredWriter : DefaultWriter
9
- TestRunner.new(Pusher.new(argv[0]), writer.new(STDOUT), DirectoryReader.new).run_tests(argv[1])
9
+ if argv[1] == "devices"
10
+ DeviceListRetriever.new(argv[0]).get_connected_devices.each do |device|
11
+ puts device
12
+ end
13
+ else
14
+ writer = argv.include?('--colour') ? ColouredWriter : DefaultWriter
15
+ TestRunner.new(Pusher.new(argv[0]), writer.new(STDOUT), DirectoryReader.new).run_tests(argv[1])
16
+ end
10
17
  end
11
18
  end
12
19
  end
@@ -0,0 +1,15 @@
1
+ require 'restclient'
2
+
3
+ module Spassky::Client
4
+ class DeviceListRetriever
5
+
6
+ def initialize(url)
7
+ @url = url
8
+ end
9
+
10
+ def get_connected_devices
11
+ JSON.parse(RestClient.get(@url + "/devices/list"))
12
+ end
13
+
14
+ end
15
+ end
@@ -27,6 +27,9 @@ module Spassky::Client
27
27
  def post_test(options)
28
28
  location = nil
29
29
  RestClient.post(test_runs_url, options) do |response, request, result|
30
+ if response.code == 500
31
+ raise response.to_str
32
+ end
30
33
  location = response.headers[:location]
31
34
  end
32
35
  raise "Expected #{test_runs_url} to respond with 302" unless location
@@ -3,6 +3,7 @@ require 'spassky/server/random_string_generator'
3
3
  require 'spassky/server/test_run'
4
4
  require 'spassky/server/device_list'
5
5
  require 'spassky/server/html_test'
6
+ require 'spassky/server/device_database'
6
7
 
7
8
  module Spassky::Server
8
9
  class App < Sinatra::Base
@@ -15,13 +16,23 @@ module Spassky::Server
15
16
  @device_list.clear
16
17
  end
17
18
 
19
+ get "/devices/list" do
20
+ @device_list.recently_connected_devices.to_json
21
+ end
22
+
18
23
  get '/device/connect' do
19
24
  redirect idle_url
20
25
  end
21
26
 
27
+ def get_device_identifier user_agent
28
+ SingletonDeviceDatabase.instance.device_identifier(user_agent)
29
+ end
30
+
22
31
  get '/device/idle/:random' do
23
- test_run = TestRun.find_next_to_run_for_user_agent(request.user_agent)
24
- @device_list.update_last_connected(request.user_agent)
32
+ device_identifier = get_device_identifier(request.user_agent)
33
+
34
+ test_run = TestRun.find_next_to_run_for_user_agent(device_identifier)
35
+ @device_list.update_last_connected(device_identifier)
25
36
  if test_run
26
37
  redirect_to_run_tests(test_run)
27
38
  else
@@ -30,12 +41,17 @@ module Spassky::Server
30
41
  end
31
42
 
32
43
  post '/test_runs' do
33
- run = TestRun.create({
34
- :name => params[:name],
35
- :contents => JSON.parse(params[:contents]),
36
- :devices => @device_list.recently_connected_devices
37
- })
38
- redirect "/test_runs/#{run.id}"
44
+ recently_connected_devices = @device_list.recently_connected_devices
45
+ if recently_connected_devices.size > 0
46
+ run = TestRun.create({
47
+ :name => params[:name],
48
+ :contents => JSON.parse(params[:contents]),
49
+ :devices => @device_list.recently_connected_devices
50
+ })
51
+ redirect "/test_runs/#{run.id}"
52
+ else
53
+ halt 500, "There are no connected devices"
54
+ end
39
55
  end
40
56
 
41
57
  get '/test_runs/:id' do
@@ -0,0 +1,39 @@
1
+ require "wurfl-lite"
2
+ require "singleton"
3
+
4
+ module Spassky::Server
5
+ LATEST = 'http://downloads.sourceforge.net/project/wurfl/WURFL/latest/wurfl-latest.xml.gz'
6
+ WURFL_FILE = "wurfl/wurfl-latest.xml.gz"
7
+
8
+ class DeviceNotFoundError < StandardError
9
+ end
10
+
11
+ class DeviceDatabase
12
+ def initialize
13
+ download_wurfl_file unless File.exist?(WURFL_FILE)
14
+ @wurfl = WURFL.new(WURFL_FILE)
15
+ end
16
+
17
+ def download_wurfl_file
18
+ Kernel.puts("Downloading WURFL database")
19
+ content = RestClient.get(LATEST)
20
+ File.open(WURFL_FILE, "w") do |file|
21
+ file.write(content)
22
+ end
23
+ end
24
+
25
+ def device_identifier user_agent
26
+ device = @wurfl[user_agent]
27
+ return user_agent if device.nil?
28
+ "#{device.model_name} (id = #{device.id}, mobile_browser = #{device.mobile_browser}, device_os_version = #{device.device_os_version})"
29
+ end
30
+
31
+ def device user_agent
32
+ @wurfl[user_agent]
33
+ end
34
+ end
35
+
36
+ class SingletonDeviceDatabase < DeviceDatabase
37
+ include Singleton
38
+ end
39
+ end
@@ -1,18 +1,21 @@
1
1
  module Spassky::Server
2
2
  class DeviceList
3
+ def initialize
4
+ @devices_and_time_last_connected = {}
5
+ end
6
+
3
7
  def update_last_connected user_agent
4
- @devices_and_time_last_connected ||= {}
5
- @devices_and_time_last_connected[user_agent] = Time.now
8
+ @devices_and_time_last_connected[user_agent] = Time.now
6
9
  end
7
-
10
+
8
11
  def recently_connected_devices
9
12
  @devices_and_time_last_connected.keys.select do |user_agent|
10
13
  Time.now.to_f - @devices_and_time_last_connected[user_agent].to_f < 3
11
14
  end
12
15
  end
13
-
16
+
14
17
  def clear
15
18
  @devices_and_time_last_connected = {}
16
19
  end
17
20
  end
18
- end
21
+ end
@@ -6,7 +6,7 @@ module Spassky
6
6
  def initialize device_statuses
7
7
  @device_statuses = device_statuses
8
8
  end
9
-
9
+
10
10
  def status
11
11
  statuses = @device_statuses.map { |s| s.status }.uniq
12
12
  return "in progress" if statuses.include?("in progress") || statuses.size == 0
@@ -14,15 +14,15 @@ module Spassky
14
14
  return "timed out" if statuses.include?("timed out")
15
15
  "pass"
16
16
  end
17
-
17
+
18
18
  def count_fails
19
19
  @device_statuses.count { |s| s.status == "fail" }
20
20
  end
21
-
21
+
22
22
  def count_timeouts
23
23
  @device_statuses.count { |s| s.status == "timed out" }
24
24
  end
25
-
25
+
26
26
  def completed_since(older_test_result)
27
27
  if older_test_result.nil?
28
28
  device_statuses.select { |s| s.completed? }
@@ -30,7 +30,7 @@ module Spassky
30
30
  find_newly_completed_device_results(older_test_result)
31
31
  end
32
32
  end
33
-
33
+
34
34
  def summary
35
35
  result = "?"
36
36
  count = @device_statuses.size
@@ -48,7 +48,7 @@ module Spassky
48
48
  result << "s" if @device_statuses.size > 1
49
49
  return result
50
50
  end
51
-
51
+
52
52
  def to_json
53
53
  {
54
54
  :status => "pass",
@@ -61,7 +61,7 @@ module Spassky
61
61
  end
62
62
  }.to_json
63
63
  end
64
-
64
+
65
65
  def self.from_json json
66
66
  parsed = JSON.parse(json)
67
67
  test_result = TestResult.new(
@@ -70,9 +70,9 @@ module Spassky
70
70
  end
71
71
  )
72
72
  end
73
-
73
+
74
74
  private
75
-
75
+
76
76
  def find_newly_completed_device_results(older_test_result)
77
77
  completed = []
78
78
  before_and_after(older_test_result) do |before, after|
@@ -82,29 +82,29 @@ module Spassky
82
82
  end
83
83
  completed
84
84
  end
85
-
85
+
86
86
  def before_and_after(older_test_result)
87
87
  device_statuses.each_with_index do |s, i|
88
88
  yield older_test_result.device_statuses[i], s
89
89
  end
90
90
  end
91
91
  end
92
-
92
+
93
93
  class DeviceTestStatus
94
94
  attr_reader :user_agent, :status, :test_name
95
-
95
+
96
96
  def initialize(user_agent, status, test_name)
97
97
  @user_agent = user_agent
98
98
  @status = status
99
99
  @test_name = test_name
100
100
  end
101
-
101
+
102
102
  def in_progress?
103
103
  @status == "in progress"
104
104
  end
105
-
105
+
106
106
  def completed?
107
107
  @status != "in progress"
108
108
  end
109
109
  end
110
- end
110
+ end
@@ -1,3 +1,3 @@
1
1
  module Spassky
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.1'
3
3
  end
data/spassky.gemspec CHANGED
@@ -22,10 +22,12 @@ Gem::Specification.new do |s|
22
22
  s.add_dependency 'json'
23
23
  s.add_dependency 'sinatra'
24
24
  s.add_dependency 'rainbow'
25
+ s.add_dependency 'wurfl-lite'
25
26
 
26
27
  s.add_development_dependency 'rake'
27
28
  s.add_development_dependency 'rspec'
28
29
  s.add_development_dependency 'cucumber'
29
30
  s.add_development_dependency 'capybara'
30
31
  s.add_development_dependency 'aruba'
32
+ s.add_development_dependency 'fakefs'
31
33
  end
@@ -44,5 +44,31 @@ module Spassky::Client
44
44
  Cli::run(["server_name", "test_name", "--colour"])
45
45
  end
46
46
  end
47
+
48
+ context "with devices as the second argument" do
49
+ it "creates a new device list retriever with the passed in url" do
50
+ device_list_retriever = mock :device_list_retriever
51
+ device_list_retriever.stub!(:get_connected_devices).and_return([])
52
+ DeviceListRetriever.should_receive(:new).with("http://localhost:9000").and_return(device_list_retriever)
53
+ Cli::run(["http://localhost:9000", "devices"])
54
+ end
55
+
56
+ it "gets a list of devices" do
57
+ device_list_retriever = mock :device_list_retriever
58
+ device_list_retriever.should_receive(:get_connected_devices).and_return([])
59
+ DeviceListRetriever.stub!(:new).and_return(device_list_retriever)
60
+ Cli::run(["http://localhost:9000", "devices"])
61
+ end
62
+
63
+ it "outputs a list of devices" do
64
+ device_list_retriever = mock :device_list_retriever
65
+ device_list_retriever.stub!(:get_connected_devices).and_return(["iphone", "ipad", "nokia"])
66
+ DeviceListRetriever.stub!(:new).and_return(device_list_retriever)
67
+ Cli.should_receive(:puts).with("iphone")
68
+ Cli.should_receive(:puts).with("ipad")
69
+ Cli.should_receive(:puts).with("nokia")
70
+ Cli::run(["http://localhost:9000", "devices"])
71
+ end
72
+ end
47
73
  end
48
74
  end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Spassky::Client
4
+ describe DeviceListRetriever do
5
+ it "retrieves the device list from the server" do
6
+ devices = ["iphone", "nokia"]
7
+ RestClient.should_receive(:get).with("http://localhost:9292/devices/list").and_return(devices.to_json)
8
+ DeviceListRetriever.new("http://localhost:9292").get_connected_devices
9
+ end
10
+
11
+ it "returns a list of devices" do
12
+ devices = ["iphone", "nokia"]
13
+ RestClient.stub(:get).and_return(devices.to_json)
14
+ returned_devices = DeviceListRetriever.new("http://localhost:9292").get_connected_devices
15
+ returned_devices.should == devices
16
+ end
17
+ end
18
+ end
@@ -43,6 +43,16 @@ module Spassky::Client
43
43
  }.should raise_error("Expected http://foo/test_runs to respond with 302")
44
44
  end
45
45
 
46
+ it "raises the error when the response is a 500" do
47
+ @response = mock("this is the error", :code => 500, :headers => { }, :to_str => "this is the error")
48
+ RestClient.stub!(:post).with("http://foo/test_runs", "test contents"
49
+ ).and_yield(@response, nil, nil)
50
+ lambda {
51
+ @pusher.push("test contents") do |result|
52
+ end
53
+ }.should raise_error("this is the error")
54
+ end
55
+
46
56
  it "polls the URL returned until the test passes" do
47
57
  RestClient.should_receive(:get).with("http://poll/me").and_return(in_progress_status, in_progress_status, in_progress_status, passed_status)
48
58
  @pusher.push("test contents") do |result|
@@ -14,36 +14,28 @@ module Spassky::Client
14
14
  @test_runner = TestRunner.new(@test_pusher, @writer, @directory_reader)
15
15
  end
16
16
 
17
- def new_in_progress_test_result
18
- test_result = mock :in_progress_test_result
19
- test_result.stub!(:status).and_return "in progress"
20
- test_result.stub!(:summary).and_return "in progress summary"
17
+ def new_test_result status, summary
18
+ test_result = mock :"#{status.gsub(" ", "_")}_test_result"
19
+ test_result.stub!(:status).and_return status
20
+ test_result.stub!(:summary).and_return summary
21
21
  test_result.stub!(:completed_since).and_return([])
22
22
  test_result
23
23
  end
24
24
 
25
+ def new_in_progress_test_result
26
+ new_test_result "in progress", "in progress summary"
27
+ end
28
+
25
29
  def new_passed_test_result
26
- test_result = mock :passed_test_result
27
- test_result.stub!(:status).and_return "pass"
28
- test_result.stub!(:summary).and_return "pass summary"
29
- test_result.stub!(:completed_since).and_return([])
30
- test_result
30
+ new_test_result "pass", "pass summary"
31
31
  end
32
32
 
33
33
  def new_failed_test_result
34
- test_result = mock :failed_test_result
35
- test_result.stub!(:status).and_return "fail"
36
- test_result.stub!(:summary).and_return "fail summary"
37
- test_result.stub!(:completed_since).and_return([])
38
- test_result
34
+ new_test_result "fail", "fail summary"
39
35
  end
40
36
 
41
37
  def new_timeout_test_result
42
- test_result = mock :timeout_test_result
43
- test_result.stub!(:status).and_return "timed out"
44
- test_result.stub!(:summary).and_return "timed out summary"
45
- test_result.stub!(:completed_since).and_return([])
46
- test_result
38
+ new_test_result "timed out", "timed out summary"
47
39
  end
48
40
 
49
41
  it "reads a test" do