aspec 0.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,16 @@
1
+ aspec
2
+ =====
3
+
4
+ External api surface testing library
5
+
6
+ # Installing
7
+
8
+ gem install aspec
9
+
10
+ # Running
11
+
12
+ aspec spec/thing_aspec.rb
13
+
14
+ # Tests
15
+
16
+ rspec spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push(File.expand_path("../../lib", __FILE__))
4
+ require 'aspec'
5
+
6
+ Aspec::CLI.new(Dir.pwd, ARGV).run
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+
3
+ require 'rack/test'
4
+ require 'rspec/mocks/standalone'
5
+ require 'term/ansicolor'
6
+ require 'json'
7
+
8
+ require 'aspec/cli'
9
+ require 'aspec/formatters/terminal'
10
+ require 'aspec/formatters/junit'
11
+ require 'aspec/runner'
12
+ require 'aspec/parser'
13
+ require 'aspec/test'
14
+
15
+ module Aspec
16
+ def self.configuration
17
+ @configuration ||= Configure.new
18
+ end
19
+
20
+ def self.configure
21
+ yield configuration
22
+ end
23
+
24
+ class Configure
25
+ attr_accessor :verbose, :slow, :formatter
26
+
27
+ def verbose?; verbose; end
28
+ def slow?; slow; end
29
+
30
+ def app_under_test(&block)
31
+ @app_under_test = block
32
+ end
33
+
34
+ def before(&block)
35
+ @before = block
36
+ end
37
+
38
+ def get_app_under_test
39
+ @app_under_test
40
+ end
41
+
42
+ def get_before
43
+ @before
44
+ end
45
+
46
+ def after_suite(&block)
47
+ @after_suite = block
48
+ end
49
+
50
+ def get_after_suite
51
+ @after_suite
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,45 @@
1
+
2
+ module Aspec
3
+ class CLI
4
+ attr_reader :args, :working_dir
5
+
6
+ def initialize(working_dir, args)
7
+ @working_dir = working_dir
8
+ @args = args
9
+ end
10
+
11
+ def aspec_files
12
+ files = []
13
+ @args.each do |arg|
14
+ arg = File.expand_path(arg, working_dir)
15
+ if File.exist?(arg)
16
+ if File.directory?(arg)
17
+ files += Dir[arg + "**/*.aspec"]
18
+ elsif arg =~ /.*\.aspec/
19
+ files << arg
20
+ end
21
+ end
22
+ end
23
+ files
24
+ end
25
+
26
+ def aspec_helper_path
27
+ File.expand_path("aspec/aspec_helper.rb")
28
+ end
29
+
30
+ def run
31
+ bits = args[0].split(":")
32
+
33
+ load aspec_helper_path if File.exist?(aspec_helper_path)
34
+
35
+ @lines = bits[1..-1].map(&:to_i)
36
+ is_verbose = args.include?("-v")
37
+ Aspec.configure do |c|
38
+ c.verbose = is_verbose
39
+ c.slow = args.include?("--slow")
40
+ c.formatter = args.include?("--junit") ? Formatter::JUnit.new(@file) : Formatter::Terminal.new(is_verbose)
41
+ end
42
+ TestRunner.new(Aspec.configuration, aspec_files).run(@lines)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,92 @@
1
+
2
+ module Aspec
3
+ module Formatter
4
+ class JUnit
5
+ def initialize(test_file_name, verbose = false, out_file_name = '.junit_aspecs')
6
+ @test_results = { :failures => [], :successes => [] }
7
+ @out = File.open(out_file_name, 'w')
8
+ @test_file_name = test_file_name
9
+ @exceptions = []
10
+ at_exit do
11
+ unless @out.closed?
12
+ @out.flush
13
+ @out.close
14
+ puts "Output junit results to .junit_aspecs"
15
+ end
16
+ end
17
+ end
18
+
19
+ def clear
20
+ end
21
+
22
+ def comment(comment_string)
23
+ end
24
+
25
+ def step_error(step)
26
+ @test_results[:failures] << [step_line(step), @exceptions]
27
+ @exceptions = []
28
+ end
29
+
30
+ def exception(error_string)
31
+ @exceptions << error_string
32
+ end
33
+
34
+ def step_error_title(step)
35
+ end
36
+
37
+ def step_pass(step)
38
+ @test_results[:successes] << step_line(step)
39
+ end
40
+
41
+ def debug
42
+ end
43
+
44
+ def dump_summary(summary)
45
+ @out.puts("<?xml version=\"1.0\" encoding=\"utf-8\" ?>")
46
+ @out.puts("<testsuite errors=\"0\" failures=\"#{failure_count}\" tests=\"#{example_count}\" time=\"#{duration=0}\" timestamp=\"#{Time.now.iso8601}\">")
47
+ @out.puts(" <properties />")
48
+
49
+ @test_results[:successes].each do |success_string|
50
+ #TODO: Add timings
51
+ runtime = 0
52
+ @out.puts(" <testcase classname=\"#{@test_file_name}\" name=\"#{test_name(success_string)}\" time=\"#{runtime}\" />")
53
+ end
54
+ @test_results[:failures].each do |(failure_string, exceptions)|
55
+ runtime = 0
56
+ @out.puts(" <testcase classname=\"#{@test_file_name}\" name=\"#{failure_string}\" time=\"#{runtime}\">")
57
+
58
+ @out.puts(" <failure message=\"failure\" type=\"failure\">")
59
+ @out.puts("<![CDATA[ #{exceptions} ]]>")
60
+ @out.puts(" </failure>")
61
+ @out.puts(" </testcase>")
62
+ end
63
+ @out.puts("</testsuite>")
64
+ end
65
+
66
+ private
67
+ def step_line(step)
68
+ "#{step[:method]} #{step[:url]} line: #{step[:line_num]}"
69
+ end
70
+
71
+ def test_name(string)
72
+ xml_encode(string.split("\n")[0].strip)
73
+ end
74
+
75
+ def example_count
76
+ @test_results[:successes].count + failure_count
77
+ end
78
+
79
+ def failure_count
80
+ @test_results[:failures].count
81
+ end
82
+
83
+ def xml_encode(string)
84
+ #TODO: Use builder to do this
85
+ string.gsub!('&', '&amp;')
86
+ string.gsub!('>', '')
87
+ string.gsub!('"','&quot;')
88
+ string
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,80 @@
1
+
2
+ module Aspec
3
+ module Formatter
4
+ class Terminal
5
+ include Term::ANSIColor
6
+
7
+ def initialize(verbose, out = STDOUT)
8
+ @out = out
9
+ @verbose = verbose
10
+ @line_buffer ||= []
11
+ end
12
+
13
+ def clear
14
+ @line_buffer.clear
15
+ end
16
+
17
+ def comment(comment_string)
18
+ line(comment_string)
19
+ end
20
+
21
+ def exception(error_string)
22
+ line(error_string)
23
+ end
24
+
25
+ def step_error_title(step)
26
+ step_line(step)
27
+ end
28
+
29
+ def step_error(step)
30
+ line(red + step_line(step) + reset)
31
+ print_error unless @verbose
32
+ end
33
+
34
+ def step_pass(step)
35
+ line(green + step_line(step) + reset)
36
+ end
37
+
38
+ def debug(step)
39
+ @out.puts(step.inspect)
40
+ end
41
+
42
+ def dump_summary(summary)
43
+ @out.puts summary
44
+ end
45
+
46
+ private
47
+
48
+ def step_line(step)
49
+ bits = [step[:method].rjust(7, " "), step[:url].ljust(50, " "), step[:exp_status], (step[:exp_content_type]||"")]
50
+ if step[:exp_content_type] == "application/json"
51
+ begin
52
+ json_string = JSON.parse(step[:exp_response]).to_json
53
+ if json_string.length > 20
54
+ json_string = "\n" + JSON.pretty_generate(JSON.parse(step[:exp_response])).split("\n").map {|l| " \\ #{l}"}.join("\n")
55
+ end
56
+ bits << json_string
57
+ rescue JSON::ParserError
58
+ bits << step[:exp_response]
59
+ end
60
+ else
61
+ bits << step[:exp_response]
62
+ end
63
+ bits.join("\t")
64
+ end
65
+
66
+ def line(line_string)
67
+ @out.puts(line_string) if @verbose
68
+ @line_buffer << line_string
69
+ end
70
+
71
+ def print_error
72
+ @line_buffer.each do |line|
73
+ @out.puts line
74
+ end
75
+ @line_buffer = []
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,50 @@
1
+ module Aspec
2
+ class Parser
3
+ def initialize(source)
4
+ @lines = source.split("\n").map {|l| l.strip}
5
+ end
6
+
7
+ def tests
8
+ @tests ||= begin
9
+ tests = [[]]
10
+ @lines.each_with_index do |line, line_num|
11
+ if line =~ /^\s*$/
12
+ if tests.last.length > 0
13
+ tests << []
14
+ end
15
+ elsif line =~ /^\s*\\(.*)$/
16
+ tests.last.last[:exp_response] = (tests.last.last[:exp_response] + $1)
17
+ else
18
+ tests.last << parse_line(line, line_num)
19
+ end
20
+ end
21
+ tests.select {|tests| tests.any?}.map {|steps| Test.new(steps) }
22
+ end
23
+ end
24
+
25
+ def parse_line(line, line_num)
26
+ if line =~ /^\s*(#.*)$/
27
+ {:comment => $1, :line_num => line_num}
28
+ else
29
+ bits = line.split(" ")
30
+ method = bits[0]
31
+ url = bits[1]
32
+ url = URI.encode(url)
33
+
34
+ exp_status = bits[2]
35
+ exp_status = exp_status.strip if exp_status
36
+ exp_content_type = bits[3]
37
+ exp_content_type = exp_content_type.strip if exp_content_type
38
+ exp_response = (bits[4..-1]||[]).join(" ")
39
+ is_regex = exp_response[0] == '/' and exp_response[-1] == '/' and exp_response.size > 2
40
+ exp_response = exp_response[1 .. -2] if is_regex
41
+
42
+ {:method => method, :url => url,
43
+ :exp_status => exp_status, :exp_content_type => exp_content_type, :exp_response => exp_response,
44
+ :resp_is_regex => is_regex, :line_num => line_num
45
+ }
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,76 @@
1
+
2
+ module Aspec
3
+ class TestRunner
4
+ include Term::ANSIColor
5
+
6
+ attr_reader :config, :source
7
+
8
+ def initialize(config, paths)
9
+ @paths = paths
10
+ @config = config
11
+ end
12
+
13
+ def verbose?
14
+ config.verbose?
15
+ end
16
+
17
+ def slow?
18
+ config.slow?
19
+ end
20
+
21
+ def formatter
22
+ config.formatter
23
+ end
24
+
25
+ def tests
26
+ @tests ||= begin
27
+ result = []
28
+ @paths.each do |path|
29
+ parser = Parser.new(File.read(path))
30
+ result += parser.tests
31
+ end
32
+ result.flatten
33
+ end
34
+ end
35
+
36
+ def before_each
37
+ if before_block = config.get_before
38
+ before_block.call
39
+ end
40
+ end
41
+
42
+ def run(lines)
43
+ successes = 0
44
+ failures = 0
45
+ if lines.any?
46
+ run_tests = tests.select {|test| lines.any? {|line_num| test.contains_line?(line_num)}}
47
+ else
48
+ run_tests = tests
49
+ end
50
+
51
+ run_tests.each do |test|
52
+ before_each
53
+ if test.run(config)
54
+ successes += 1 unless test.comment_only?
55
+ puts if verbose?
56
+ else
57
+ failures += 1
58
+ puts
59
+ end
60
+ formatter.clear
61
+ end
62
+ color = send(failures > 0 ? :red : :green)
63
+ formatter.dump_summary color + "#{successes} passed, #{failures} failed." + reset
64
+
65
+ if after_suite_block = config.get_after_suite
66
+ after_suite_block.call
67
+ end
68
+ if failures > 0
69
+ exit(1)
70
+ else
71
+ exit(0)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,140 @@
1
+
2
+ module Aspec
3
+ class Test
4
+ include Rack::Test::Methods
5
+
6
+ def initialize(steps)
7
+ @steps = steps
8
+ end
9
+
10
+ def validate_method(method)
11
+ raise "unknown method #{method}" unless %W(GET POST DELETE PUT).include?(method)
12
+ end
13
+
14
+ def comment_only?
15
+ @steps.all? {|s| s[:comment]}
16
+ end
17
+
18
+ def contains_line?(line_num)
19
+ @steps.first[:line_num] <= line_num and @steps.last[:line_num] >= line_num
20
+ end
21
+
22
+ def app
23
+ @app
24
+ end
25
+
26
+ def run(config)
27
+ formatter = config.formatter
28
+ is_slow = config.slow?
29
+ @app = config.get_app_under_test.call
30
+ start_time = Time.at(0)
31
+ failed = false
32
+ @steps.each_with_index do |step, time_delta|
33
+ if is_slow
34
+ sleep 0.5
35
+ end
36
+
37
+ if ARGV.include?("--debug")
38
+ formatter.debug(step)
39
+ end
40
+
41
+ if failed
42
+ formatter.step_error_title(step)
43
+ next
44
+ end
45
+
46
+ if step[:comment]
47
+ formatter.comment(step[:comment])
48
+ else
49
+ Time.stub!(:now).and_return(start_time + 2*time_delta)
50
+
51
+ begin
52
+ if step[:method][0] == ">"
53
+ method = step[:method][1..-1]
54
+ validate_method(method)
55
+ url = "http://" + step[:url]
56
+ FakeWeb.register_uri(method.downcase.to_sym, url,
57
+ :body => step[:exp_response],
58
+ :content_type => step[:exp_content_type]
59
+ )
60
+ else
61
+ validate_method(step[:method])
62
+ send(step[:method].downcase, step[:url])
63
+ end
64
+ rescue Object => e
65
+ formatter.exception(" " + e.class.to_s + ": " + e.message)
66
+ e.backtrace.each do |backtrace_line|
67
+ formatter.exception(" " + backtrace_line) unless backtrace_line =~ /vendor\/bundle/ or backtrace_line =~ /test.rb/
68
+ end
69
+ failed = true
70
+ end
71
+
72
+ unless failed or step[:method][0] == ">"
73
+ if last_response.status.to_s != step[:exp_status]
74
+ formatter.exception(" * Expected status #{step[:exp_status]} got #{last_response.status}")
75
+ failed = true
76
+ end
77
+
78
+ if step[:exp_content_type] == "application/json" && !step[:resp_is_regex]
79
+ begin
80
+ expected_object = JSON.parse(step[:exp_response])
81
+ begin
82
+ response_object = JSON.parse(last_response.body)
83
+ if expected_object != response_object
84
+ formatter.exception(" * Expected response #{JSON.pretty_generate(expected_object)} got #{JSON.pretty_generate(response_object)}")
85
+ failed = true
86
+ end
87
+ rescue JSON::ParserError
88
+ formatter.exception(" * Response did not parse correctly as JSON: #{last_response.body.inspect}")
89
+ failed = true
90
+ end
91
+ rescue JSON::ParserError
92
+ formatter.exception(" * Expectation did not parse correctly as JSON: #{step[:exp_response].inspect}")
93
+ failed = true
94
+ end
95
+
96
+ else
97
+
98
+ if step[:resp_is_regex]
99
+ pattern = nil, body = nil
100
+ if !(step[:exp_content_type].start_with? 'text/')
101
+ pattern = Regexp.new(step[:exp_response].force_encoding("ASCII-8BIT"), Regexp::FIXEDENCODING)
102
+ body = last_response.body.to_s.force_encoding("ASCII-8BIT")
103
+ else
104
+ pattern = Regexp.new(step[:exp_response])
105
+ body = last_response.body.to_s
106
+ end
107
+ if !(body =~ pattern)
108
+ formatter.exception(" * Expected response pattern #{step[:exp_response].inspect} didn't match #{last_response.body.inspect}")
109
+ failed = true
110
+ end
111
+ elsif !step[:resp_is_regex] & (last_response.body.to_s != step[:exp_response])
112
+ formatter.exception(" * Expected response #{step[:exp_response].inspect} got #{last_response.body.inspect[0..50] + "..."}")
113
+ failed = true
114
+ end
115
+ end
116
+
117
+ if step[:exp_content_type]
118
+ exp_content_type_header = "#{step[:exp_content_type]}"
119
+ exp_content_type_header << ";charset=utf-8" unless exp_content_type_header.start_with? "image/"
120
+ if last_response.headers["Content-Type"] != exp_content_type_header
121
+ formatter.exception(" * Expected content type #{exp_content_type_header} got #{last_response.headers["Content-Type"]}")
122
+ failed = true
123
+ end
124
+ end
125
+ end
126
+
127
+ if failed
128
+ formatter.step_error(step)
129
+ else
130
+ formatter.step_pass(step)
131
+ end
132
+ end
133
+ end
134
+ !failed
135
+ end
136
+ end
137
+ end
138
+
139
+
140
+
@@ -0,0 +1,9 @@
1
+
2
+ require 'spec_helper'
3
+
4
+ describe Aspec do
5
+ it "Should exist" do
6
+ Aspec
7
+ end
8
+
9
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aspec::CLI do
4
+ def test_app_dir
5
+ File.expand_path("../test_app", __FILE__)
6
+ end
7
+
8
+ it "should list all files in a directory" do
9
+ cli = Aspec::CLI.new(test_app_dir, ["aspec/"])
10
+ cli.aspec_files.sort.should == [
11
+ File.expand_path("aspec/failing.aspec", test_app_dir),
12
+ File.expand_path("aspec/passing.aspec", test_app_dir)].sort
13
+ end
14
+ end
@@ -0,0 +1,2 @@
1
+ $:.push(File.expand_path("../../lib", __FILE__))
2
+ require 'aspec'
@@ -0,0 +1,8 @@
1
+ $:.push(File.expand_path("../../", __FILE__))
2
+ require 'test_app'
3
+
4
+ Aspec.configure do |c|
5
+ c.app_under_test do
6
+ TestApp.new
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ class TestApp < Sinatra::Application
5
+ get "/" do
6
+ end
7
+
8
+ get "/artists" do
9
+ status 200
10
+ content_type :json
11
+ @@artists.to_json
12
+ end
13
+
14
+ post "/artists" do
15
+ @@artists ||= []
16
+ @@artists << params[:name]
17
+ status 204
18
+ nil
19
+ end
20
+
21
+ delete "/artists/:name" do
22
+ @@artists = @@artists.reject {|a| a == params[:name] }
23
+ status 204
24
+ nil
25
+ end
26
+ end
27
+
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aspec
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: "0.1"
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Lucraft
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-12-06 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: term-ansicolor
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: rack-test
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id003
48
+ description:
49
+ email: dan@songkick.com
50
+ executables:
51
+ - aspec
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - README.md
56
+ files:
57
+ - README.md
58
+ - lib/aspec.rb
59
+ - lib/aspec/parser.rb
60
+ - lib/aspec/runner.rb
61
+ - lib/aspec/cli.rb
62
+ - lib/aspec/test.rb
63
+ - lib/aspec/formatters/terminal.rb
64
+ - lib/aspec/formatters/junit.rb
65
+ - spec/spec_helper.rb
66
+ - spec/cli_spec.rb
67
+ - spec/test_app/aspec/aspec_helper.rb
68
+ - spec/test_app/test_app.rb
69
+ - spec/aspec_spec.rb
70
+ - bin/aspec
71
+ homepage: http://github.com/songkick/aspec
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options:
76
+ - --main
77
+ - README.md
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.12
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Testing for API external surfaces
99
+ test_files: []
100
+