hobby-test 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +15 -0
- data/debug.rb +7 -0
- data/hobby-test.gemspec +10 -0
- data/lib/hobby/test/exchange/assert.rb +62 -0
- data/lib/hobby/test/exchange/request.rb +16 -0
- data/lib/hobby/test/exchange.rb +31 -0
- data/lib/hobby/test/report.rb +17 -0
- data/lib/hobby/test.rb +28 -0
- data/spec/apps/main.rb +69 -0
- data/spec/auto_spec.rb +20 -0
- data/spec/helper.rb +30 -0
- data/spec/http/basic.yml +14 -0
- data/spec/http/event.yml +11 -0
- data/spec/http/reverse.yml +24 -0
- data/spec/run_mutant.rb +70 -0
- data/spec/setup/mutant.rb +24 -0
- data/spec/setup/power_assert.rb +19 -0
- data/spec/tcp_spec.rb +28 -0
- data/spec/yml/failing/0.yml +12 -0
- data/spec/yml/passing/0.yml +12 -0
- data/spec/yml/passing/chain_assertions.yml +36 -0
- data/spec/yml/passing/counter.yml +11 -0
- data/spec/yml/passing/echo.yml +23 -0
- data/spec/yml/passing/query.yml +8 -0
- data/useful_links +3 -0
- metadata +28 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6630ab85ce09d1fbd6661bd8ec6201e66a73c197
|
4
|
+
data.tar.gz: 7547c087095c379e16aafe99a62fb72dddedfba6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f2d0e876e7b6e10990418aa0318aeca79189b30f9953eb24c50fe2d212110a0c4603e5d00a5d061bb9a01f3f18219c11a07369dbbd91c6fcbb7149c3b745fa7
|
7
|
+
data.tar.gz: 6f4c9b2756eb6fd613ed6ef211a208f5ef78eb74aa81312f81da11e0d5715dc8b86da986ab0aa2cd745e3a2d4ecc6bc1f27c14d2eff4bdc92a86083d9502d141
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/debug.rb
ADDED
data/hobby-test.gemspec
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Gem::Specification.new do |g|
|
2
|
+
g.name = 'hobby-test'
|
3
|
+
g.files = `git ls-files`.split($/)
|
4
|
+
g.version = '0.0.1'
|
5
|
+
g.summary = 'A way to test HTTP exchanges via YAML specifications'
|
6
|
+
g.authors = ['Anatoly Chernow']
|
7
|
+
|
8
|
+
g.add_dependency 'excon'
|
9
|
+
g.add_dependency 'to_proc'
|
10
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Hobby::Test::Exchange
|
2
|
+
module Assert
|
3
|
+
def self.[] pair
|
4
|
+
key, delimiter, chain = pair[0].partition /\.|\[/
|
5
|
+
chain.prepend (delimiter == '[' ? 'self[' : 'self.') unless chain.empty?
|
6
|
+
const_get(key.capitalize).new key, chain, pair[1]
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize key, chain, value
|
10
|
+
@key, @chain, @specified_value = key, chain, value
|
11
|
+
end
|
12
|
+
|
13
|
+
def ok?
|
14
|
+
@ok
|
15
|
+
end
|
16
|
+
|
17
|
+
def [] response
|
18
|
+
dup.assert response
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :actual_value, :specified_value, :chain, :key
|
22
|
+
|
23
|
+
class Status
|
24
|
+
include Assert
|
25
|
+
|
26
|
+
def assert response
|
27
|
+
@actual_value = response.public_send key
|
28
|
+
@ok = actual_value == specified_value
|
29
|
+
self
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Body
|
34
|
+
include Assert
|
35
|
+
|
36
|
+
def assert response
|
37
|
+
@actual_value = response.public_send key
|
38
|
+
@actual_value = begin
|
39
|
+
JSON.parse actual_value
|
40
|
+
rescue JSON::ParserError
|
41
|
+
actual_value
|
42
|
+
end
|
43
|
+
|
44
|
+
@ok = if chain.empty?
|
45
|
+
actual_value == specified_value
|
46
|
+
else
|
47
|
+
compare_chain
|
48
|
+
end
|
49
|
+
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def compare_chain
|
54
|
+
if chain.end_with? '>', '=', '<'
|
55
|
+
actual_value.instance_eval "#{chain}(#{specified_value})"
|
56
|
+
else
|
57
|
+
(actual_value.instance_eval chain) == specified_value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Hobby::Test::Exchange
|
2
|
+
class Request < OpenStruct
|
3
|
+
VERBS = %w[delete get head options patch post put]
|
4
|
+
def initialize array
|
5
|
+
@verb = array[0]
|
6
|
+
super array[1]
|
7
|
+
end
|
8
|
+
attr_reader :verb
|
9
|
+
|
10
|
+
def to_hash
|
11
|
+
hash = to_h
|
12
|
+
hash[:body] = hash[:body].to_json
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Hobby
|
2
|
+
class Test
|
3
|
+
class Exchange
|
4
|
+
def initialize hash
|
5
|
+
@request = Request.new hash.find { |key, _|
|
6
|
+
Request::VERBS.include? key
|
7
|
+
}
|
8
|
+
@asserts = (hash['response']&.map &Assert) || []
|
9
|
+
end
|
10
|
+
attr_reader :request, :asserts
|
11
|
+
|
12
|
+
def [] connection
|
13
|
+
dup.run_against connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def ok?
|
17
|
+
asserts.all? &:ok?
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def run_against connection
|
22
|
+
response = connection.public_send request.verb, **request
|
23
|
+
@asserts = asserts.map &[response]
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'hobby/test/exchange/request'
|
31
|
+
require 'hobby/test/exchange/assert'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Hobby
|
2
|
+
class Test
|
3
|
+
class Report
|
4
|
+
def initialize exchanges
|
5
|
+
@exchanges = exchanges
|
6
|
+
end
|
7
|
+
|
8
|
+
def ok?
|
9
|
+
@exchanges.all? &:ok?
|
10
|
+
end
|
11
|
+
|
12
|
+
include Enumerable
|
13
|
+
extend Forwardable
|
14
|
+
delegate [:each, :[], :size] => :@exchanges
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/hobby/test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'to_proc/all'
|
2
|
+
require 'excon'
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
require 'json'
|
6
|
+
require 'forwardable'
|
7
|
+
require 'ostruct'
|
8
|
+
|
9
|
+
require 'hobby/test/exchange'
|
10
|
+
require 'hobby/test/report'
|
11
|
+
|
12
|
+
module Hobby
|
13
|
+
class Test
|
14
|
+
def initialize file
|
15
|
+
@exchanges = (YAML.load_file file).map &Exchange
|
16
|
+
end
|
17
|
+
|
18
|
+
def [] address
|
19
|
+
connection = if address.start_with? 'http'
|
20
|
+
Excon.new address
|
21
|
+
else
|
22
|
+
Excon.new 'unix:///', socket: address
|
23
|
+
end
|
24
|
+
|
25
|
+
Report.new @exchanges.map &[connection]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/apps/main.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'hobby'
|
2
|
+
require 'hobby/json'
|
3
|
+
|
4
|
+
class MainApp
|
5
|
+
include Hobby::App
|
6
|
+
get { 'root-only app' }
|
7
|
+
|
8
|
+
class Counter
|
9
|
+
include Hobby::App
|
10
|
+
@@counter = 0
|
11
|
+
get { @@counter }
|
12
|
+
post { @@counter += 1 }
|
13
|
+
end
|
14
|
+
map('/counter') { run Counter.new }
|
15
|
+
|
16
|
+
class Echo
|
17
|
+
include Hobby::App
|
18
|
+
include Hobby::JSON
|
19
|
+
|
20
|
+
get { json }
|
21
|
+
end
|
22
|
+
map('/echo') { run Echo.new }
|
23
|
+
get('/echo-with-query') { request.params.to_json }
|
24
|
+
|
25
|
+
class Query
|
26
|
+
include Hobby::App
|
27
|
+
get { request.params['array'].class }
|
28
|
+
end
|
29
|
+
map('/query') { run Query.new }
|
30
|
+
|
31
|
+
class HashApp
|
32
|
+
include Hobby::App
|
33
|
+
include Hobby::JSON
|
34
|
+
get do
|
35
|
+
{
|
36
|
+
'a' => 1,
|
37
|
+
'b' => 2,
|
38
|
+
'c' => 3
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
map('/hash') { run HashApp.new }
|
43
|
+
|
44
|
+
class ArrayApp
|
45
|
+
include Hobby::App
|
46
|
+
include Hobby::JSON
|
47
|
+
get do
|
48
|
+
[
|
49
|
+
{
|
50
|
+
'first' => {
|
51
|
+
'a' => 0, 'b' => 1
|
52
|
+
},
|
53
|
+
'second' => {
|
54
|
+
'c' => 2, 'd' => 3
|
55
|
+
}
|
56
|
+
},
|
57
|
+
{
|
58
|
+
'first' => {
|
59
|
+
'a' => 5, 'b' => 6
|
60
|
+
},
|
61
|
+
'second' => {
|
62
|
+
'c' => 2, 'd' => 3
|
63
|
+
}
|
64
|
+
}
|
65
|
+
]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
map('/array') { run ArrayApp.new }
|
69
|
+
end
|
data/spec/auto_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe 'passing and failing YAML specifications' do
|
4
|
+
Dir["spec/yml/**/*.yml"].each do |path|
|
5
|
+
name = path.split('/').last
|
6
|
+
test = Hobby::Test.new path
|
7
|
+
|
8
|
+
if path.include? 'passing'
|
9
|
+
it "passing #{name}" do
|
10
|
+
report = test[@socket]
|
11
|
+
assert { report.ok? }
|
12
|
+
end
|
13
|
+
else
|
14
|
+
it "failing #{name}" do
|
15
|
+
report = test[@socket]
|
16
|
+
assert { not report.ok? }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'setup/power_assert'
|
2
|
+
require_relative 'setup/mutant'
|
3
|
+
require_relative 'apps/main'
|
4
|
+
|
5
|
+
require 'hobby/test'
|
6
|
+
require 'puma'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.expect_with :rspec, :minitest
|
10
|
+
|
11
|
+
config.before :each do |example|
|
12
|
+
@socket = "MainApp.for.#{example}.socket"
|
13
|
+
@pid = fork do
|
14
|
+
server = Puma::Server.new MainApp.new
|
15
|
+
server.add_unix_listener @socket
|
16
|
+
server.run
|
17
|
+
sleep
|
18
|
+
end
|
19
|
+
|
20
|
+
sleep 0.01 until File.exist? @socket
|
21
|
+
end
|
22
|
+
|
23
|
+
config.after :each do
|
24
|
+
`kill #{@pid}`
|
25
|
+
end
|
26
|
+
|
27
|
+
config.after :suite do
|
28
|
+
`rm *.socket` unless Dir['*.socket'].empty?
|
29
|
+
end
|
30
|
+
end
|
data/spec/http/basic.yml
ADDED
data/spec/http/event.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
- default:
|
2
|
+
verb: post
|
3
|
+
path: /event
|
4
|
+
format: json
|
5
|
+
|
6
|
+
- request:
|
7
|
+
body:
|
8
|
+
text: text
|
9
|
+
|
10
|
+
response:
|
11
|
+
status: 201
|
12
|
+
body:
|
13
|
+
name: ReverseApp
|
14
|
+
text: txet
|
15
|
+
|
16
|
+
- request:
|
17
|
+
body:
|
18
|
+
text: reverse
|
19
|
+
|
20
|
+
response:
|
21
|
+
status: 201
|
22
|
+
body:
|
23
|
+
name: ReverseApp
|
24
|
+
text: something entirely else
|
data/spec/run_mutant.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# A workaround for
|
2
|
+
# https://github.com/mbj/mutant/#the-crash--stuck-problem-mri
|
3
|
+
#
|
4
|
+
# For Hobby::Test::Report
|
5
|
+
# https://gist.github.com/ch1c0t/c5e0a8de8de14d10bec49839fb6e2fee
|
6
|
+
# this mutation
|
7
|
+
# https://gist.github.com/ch1c0t/c1626fe232032489fad93dab8d3a10c0
|
8
|
+
# causes the infinite running.
|
9
|
+
#
|
10
|
+
# This workaround "solves" it by introducing a timeout enforced
|
11
|
+
# from the parent process.
|
12
|
+
#
|
13
|
+
# With this workaround, Mutant ends its run successfully, but lefts out
|
14
|
+
# a process which keeps running in the background. That process, apparently,
|
15
|
+
# is forked from the process with the offending mutation.
|
16
|
+
# TODO: find out why it happens; create a workaround for the workaround
|
17
|
+
# (because killing that process manually every time is too much hassle).
|
18
|
+
require 'mutant'
|
19
|
+
|
20
|
+
Runner = Mutant::Runner
|
21
|
+
Bootstrap = Mutant::Env::Bootstrap
|
22
|
+
Integration = Mutant::Integration
|
23
|
+
|
24
|
+
require 'tra/run'
|
25
|
+
module MyIsolation
|
26
|
+
def self.call &block
|
27
|
+
pid = fork do
|
28
|
+
Process.setproctitle Process.pid.to_s
|
29
|
+
Process.ppid.put block.call
|
30
|
+
end
|
31
|
+
Timeout.timeout(3) { Tra.next }
|
32
|
+
rescue
|
33
|
+
Process.kill :KILL, pid
|
34
|
+
fail Mutant::Isolation::Error
|
35
|
+
ensure
|
36
|
+
Process.waitpid pid
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MyIntegration
|
41
|
+
def initialize _config
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup
|
45
|
+
self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
DEFAULT = Mutant::Config::DEFAULT
|
50
|
+
config = DEFAULT.with \
|
51
|
+
matcher: DEFAULT.matcher.add(:match_expressions, DEFAULT.expression_parser.(ARGV[0])),
|
52
|
+
integration: Integration.setup(Kernel, 'rspec'),
|
53
|
+
isolation: MyIsolation
|
54
|
+
|
55
|
+
#Runner.call Bootstrap.call config
|
56
|
+
|
57
|
+
env = Bootstrap.call config
|
58
|
+
puts "#{env.mutations.size} mutations are to be checked."
|
59
|
+
result_mutations = env.mutations.map.with_index 1 do |mutation, index|
|
60
|
+
puts index
|
61
|
+
env.kill mutation
|
62
|
+
end
|
63
|
+
|
64
|
+
failed_mutations = result_mutations.reject &:success?
|
65
|
+
if failed_mutations.empty?
|
66
|
+
puts 'Covered.'
|
67
|
+
else
|
68
|
+
require 'pry'
|
69
|
+
failed_mutations.__binding__.pry
|
70
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
if defined? Mutant::CLI
|
2
|
+
module Mutant
|
3
|
+
class Selector::Expression
|
4
|
+
def call _subject
|
5
|
+
integration.all_tests
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'timeout'
|
10
|
+
class Isolation::Fork
|
11
|
+
def parent reader, writer, &block
|
12
|
+
pid = process.fork do
|
13
|
+
child reader, writer, &block
|
14
|
+
end
|
15
|
+
|
16
|
+
writer.close
|
17
|
+
Timeout::timeout(3) { marshal.load reader }
|
18
|
+
ensure
|
19
|
+
process.kill :KILL, pid
|
20
|
+
process.waitpid pid
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require 'minitest-power_assert'
|
3
|
+
|
4
|
+
assert = if ENV['PRY']
|
5
|
+
require 'pry'
|
6
|
+
require 'awesome_print'
|
7
|
+
|
8
|
+
Module.new do
|
9
|
+
def assert &block
|
10
|
+
PowerAssert.start Proc.new, assertion_method: __method__ do |pa|
|
11
|
+
block.binding.pry unless pa.yield
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
else
|
16
|
+
Minitest::PowerAssert::Assertions
|
17
|
+
end
|
18
|
+
|
19
|
+
Minitest::Assertions.prepend assert
|
data/spec/tcp_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
describe Hobby::Test do
|
2
|
+
before do
|
3
|
+
@tcp_pid = fork do
|
4
|
+
server = Puma::Server.new MainApp.new
|
5
|
+
server.add_tcp_listener 'localhost', 8080
|
6
|
+
server.run
|
7
|
+
sleep
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
Excon.get 'http://localhost:8080'
|
12
|
+
rescue
|
13
|
+
sleep 0.01
|
14
|
+
retry
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
`kill #{@tcp_pid}`
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'in case of success' do
|
24
|
+
test = described_class.new 'spec/yml/passing/0.yml'
|
25
|
+
report = test['http://localhost:8080']
|
26
|
+
assert { report.ok? }
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
- get:
|
2
|
+
path: /hash
|
3
|
+
|
4
|
+
response:
|
5
|
+
body:
|
6
|
+
a: 1
|
7
|
+
b: 2
|
8
|
+
c: 3
|
9
|
+
body.>:
|
10
|
+
a: 1
|
11
|
+
body.size: 3
|
12
|
+
body['b']: 2
|
13
|
+
|
14
|
+
- get:
|
15
|
+
path: /array
|
16
|
+
|
17
|
+
response:
|
18
|
+
body:
|
19
|
+
- first:
|
20
|
+
a: 0
|
21
|
+
b: 1
|
22
|
+
second:
|
23
|
+
c: 2
|
24
|
+
d: 3
|
25
|
+
- first:
|
26
|
+
a: 5
|
27
|
+
b: 6
|
28
|
+
second:
|
29
|
+
c: 2
|
30
|
+
d: 3
|
31
|
+
body.size: 2
|
32
|
+
body[1]['first']:
|
33
|
+
a: 5
|
34
|
+
b: 6
|
35
|
+
body[1]['first'].>:
|
36
|
+
a: 5
|
@@ -0,0 +1,23 @@
|
|
1
|
+
- get:
|
2
|
+
path: /echo-with-query
|
3
|
+
query:
|
4
|
+
first: one
|
5
|
+
second: two
|
6
|
+
|
7
|
+
response:
|
8
|
+
status: 200
|
9
|
+
body:
|
10
|
+
first: one
|
11
|
+
second: two
|
12
|
+
|
13
|
+
- get:
|
14
|
+
path: /echo
|
15
|
+
body:
|
16
|
+
first: one
|
17
|
+
second: two
|
18
|
+
|
19
|
+
response:
|
20
|
+
status: 200
|
21
|
+
body:
|
22
|
+
first: one
|
23
|
+
second: two
|
data/useful_links
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hobby-test
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anatoly Chernow
|
@@ -43,7 +43,33 @@ email:
|
|
43
43
|
executables: []
|
44
44
|
extensions: []
|
45
45
|
extra_rdoc_files: []
|
46
|
-
files:
|
46
|
+
files:
|
47
|
+
- ".gitignore"
|
48
|
+
- Gemfile
|
49
|
+
- debug.rb
|
50
|
+
- hobby-test.gemspec
|
51
|
+
- lib/hobby/test.rb
|
52
|
+
- lib/hobby/test/exchange.rb
|
53
|
+
- lib/hobby/test/exchange/assert.rb
|
54
|
+
- lib/hobby/test/exchange/request.rb
|
55
|
+
- lib/hobby/test/report.rb
|
56
|
+
- spec/apps/main.rb
|
57
|
+
- spec/auto_spec.rb
|
58
|
+
- spec/helper.rb
|
59
|
+
- spec/http/basic.yml
|
60
|
+
- spec/http/event.yml
|
61
|
+
- spec/http/reverse.yml
|
62
|
+
- spec/run_mutant.rb
|
63
|
+
- spec/setup/mutant.rb
|
64
|
+
- spec/setup/power_assert.rb
|
65
|
+
- spec/tcp_spec.rb
|
66
|
+
- spec/yml/failing/0.yml
|
67
|
+
- spec/yml/passing/0.yml
|
68
|
+
- spec/yml/passing/chain_assertions.yml
|
69
|
+
- spec/yml/passing/counter.yml
|
70
|
+
- spec/yml/passing/echo.yml
|
71
|
+
- spec/yml/passing/query.yml
|
72
|
+
- useful_links
|
47
73
|
homepage:
|
48
74
|
licenses: []
|
49
75
|
metadata: {}
|