spassky 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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