cross_spec 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d38aa581806d1683766f7ee70c3dc0236152e46a
4
+ data.tar.gz: a03f9af4c9f5d9023e7fe6e3929312c81d21e42e
5
+ SHA512:
6
+ metadata.gz: 0fb4744501e0a442bb053fe886dce327c415a165188256ea1c5145fa00475a54ce8a69c3c8abac69187eb8e86e8e734745ba8b6871ac53c3c6c15aba11984faf
7
+ data.tar.gz: 4e5d9ae00e2d9e9a849bd29b3f13f079dfad7322ef593c4868a7c7425d7832301cd9e3fdc4d6b361e2382b6c469627c80268bb66e5fdf541c798a5417cce1af8
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .byebug_history
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cross_spec.gemspec
4
+ gemspec
5
+ gem "distributed_tracing", path: "../distributed_tracing_ruby"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Ben Fischer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # CrossSpec
2
+
3
+ A utility library for marking cross app testing easier with tools for distributed factories, test parallelism, and verification of async events.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cross_spec'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ ### Listen for service requests in your app
20
+ ```ruby
21
+ CrossSpec.handle("app1:create_user") do
22
+ # create a user in here
23
+ # then tell the spec that the data is ready
24
+ CrossSpec.beacon("app1:user_created", {id: 123})
25
+ end
26
+
27
+ CrossSpec.listen! # blocks forever
28
+ ```
29
+
30
+ ### Notify of side effects in your app
31
+ ```ruby
32
+ CrossSpec.beacon("app1:background_job1_success")
33
+ ```
34
+
35
+ ### Notify of failures in your app
36
+ ```ruby
37
+ CrossSpec.failure_beacon!
38
+ ```
39
+
40
+ ### Ask for data in a spec and wait for it
41
+ ```ruby
42
+ CrossSpec::Spec.trace do
43
+ user = sync(service: "app1:create_user", {email: "email@example.com"}, tasks: "app1:user_created")
44
+ user["id"] # 123
45
+ end
46
+ ```
47
+
48
+ ### Ask for data in a spec async and wait for it later
49
+ ```ruby
50
+ CrossSpec::Spec.trace do
51
+ async(service: "app1:create_user", {email: "email@example.com"})
52
+ async(service: "app2:create_blogpost", {title: "Read me"})
53
+ # keep doing things if you want ...
54
+
55
+ # block until the data is all ready
56
+ user, blogpost = await(tasks: ["app1:user_created", "app2:blogpost_created"])
57
+ end
58
+ ```
59
+
60
+
61
+ ## Development
62
+
63
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bundle exec rspec` to run the tests.
64
+
65
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
66
+
67
+ ## Contributing
68
+
69
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cross_spec.
70
+
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
75
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cross_spec"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cross_spec/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cross_spec"
8
+ spec.version = CrossSpec::VERSION
9
+ spec.authors = ["Ben Fischer"]
10
+ spec.email = ["bfischer@doximity.com"]
11
+
12
+ spec.summary = %q{A framework for doing cross app testing with support for distributed factories, test parallelism, and verification of async events.}
13
+ spec.description = %q{A framework for doing cross app testing with support for distributed factories, test parallelism, and verification of async events.}
14
+ spec.homepage = "https://github.com/doximity/cross_spec_ruby"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.14"
25
+ spec.add_development_dependency "byebug"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "simplecov"
29
+
30
+ spec.add_dependency "ruby-kafka"
31
+ spec.add_dependency "distributed_tracing"
32
+ end
@@ -0,0 +1,35 @@
1
+ module CrossSpec
2
+ class Client
3
+ def initialize(driver, handlers = {})
4
+ @driver = driver
5
+ @handlers = Hash(handlers)
6
+ end
7
+
8
+ def broadcast(str)
9
+ @driver.broadcast(str)
10
+ end
11
+
12
+ def listen!(&block)
13
+ @driver.each_message do |message|
14
+ json = JSON.parse(message)
15
+ message = if json.key?("service")
16
+ ServiceMessage.from_json(json)
17
+ elsif json.key?("task")
18
+ TaskMessage.from_json(json)
19
+ end
20
+
21
+ next unless message
22
+
23
+ result = if block_given?
24
+ yield message
25
+ elsif message.is_a?(TaskMessage) && @handlers[message.task]
26
+ @handlers[message.task].call(message)
27
+ elsif message.is_a?(ServiceMessage) && @handlers[message.service]
28
+ @handlers[message.service].call(message)
29
+ end
30
+
31
+ return unless result.nil?
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ module CrossSpec
2
+ class Config
3
+ def client
4
+ return @client if defined?(@client)
5
+ self.client = Client.new(KafkaClient.new(kafka), handlers)
6
+ @client
7
+ end
8
+
9
+ def handlers
10
+ @handlers ||= {}
11
+ end
12
+
13
+ def kafka_connection_string
14
+ return @kafka_connection_string if defined?(@kafka_connection_string)
15
+ self.kafka_connection_string = "localhost"
16
+ @kafka_connection_string
17
+ end
18
+
19
+ attr_writer :client, :handlers
20
+
21
+ def kafka_connection_string=(value)
22
+ @kafka_connection_string = value
23
+ @kafka = Kafka.new(seed_brokers: @kafka_connection_string)
24
+ value
25
+ end
26
+
27
+ private
28
+
29
+ def kafka
30
+ @kafka ||= Kafka.new(seed_brokers: kafka_connection_string)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module CrossSpec
2
+ class KafkaClient
3
+ TOPIC = "cross-spec.messages"
4
+ def initialize(kafka)
5
+ @kafka = kafka
6
+ end
7
+
8
+ def broadcast(str)
9
+ @kafka.deliver_message(str, topic: TOPIC)
10
+ end
11
+
12
+ def each_message(&block)
13
+ consumer = @kafka.consumer(group_id: group_id)
14
+ consumer.subscribe(TOPIC)
15
+ consumer.each_message(&block)
16
+ end
17
+
18
+ def group_id
19
+ @group_id ||= "cross-spec-#{SecureRandom.hex}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module CrossSpec
2
+ class Message
3
+ def initialize(data: nil, biomarkers: nil)
4
+ @data = data
5
+ @biomarkers = Array(biomarkers)
6
+ end
7
+
8
+ attr_reader :data, :biomarkers
9
+
10
+ def origin_biomarker
11
+ @biomarkers.last
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module CrossSpec
2
+ class ServiceMessage < Message
3
+ def initialize(service:, data: nil, biomarkers: nil)
4
+ super(data: data, biomarkers: biomarkers)
5
+ @service = service
6
+ end
7
+
8
+ attr_reader :service
9
+
10
+ def self.from_json(json)
11
+ new(
12
+ service: json["service"],
13
+ data: json["data"],
14
+ biomarkers: json["biomarkers"]
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,84 @@
1
+ module CrossSpec
2
+ class SpecWaiting
3
+ def initialize(tasks)
4
+ @positive_matches = 0
5
+ @data = []
6
+ @tasks = Array(tasks)
7
+ end
8
+
9
+ def process(message, &block)
10
+ return unless message.is_a?(TaskMessage)
11
+ return unless @tasks.include?(message.task)
12
+
13
+ positive_match = if block
14
+ yield message.data, message.biomarkers
15
+ elsif DistributedTracing.get.any?
16
+ (message.biomarkers & DistributedTracing.get).any?
17
+ else
18
+ false
19
+ end
20
+
21
+ if positive_match
22
+ increment_matches
23
+ store_data(message)
24
+ end
25
+ end
26
+
27
+ def done?
28
+ @positive_matches === @tasks.length
29
+ end
30
+
31
+ def data
32
+ @data
33
+ end
34
+
35
+ private
36
+
37
+ def increment_matches
38
+ @positive_matches += 1
39
+ end
40
+
41
+ def store_data(message)
42
+ i = @tasks.index(message.task)
43
+ @data[i] = message.data
44
+ end
45
+ end
46
+
47
+ class Spec
48
+ def self.trace(&block)
49
+ spec = new
50
+ DistributedTracing.trace do
51
+ spec.instance_eval(&block)
52
+ end
53
+ end
54
+
55
+ def async(service:, data: nil)
56
+ DistributedTracing.trace do
57
+ str = JSON.dump(
58
+ service: service,
59
+ biomarkers: DistributedTracing.get,
60
+ data: data
61
+ )
62
+ CrossSpec.broadcast(str)
63
+ end
64
+ nil
65
+ end
66
+
67
+ def sync(service:, data: nil, tasks:, options: {}, &block)
68
+ async(service: service, data: data)
69
+ await(tasks: tasks, options: options, &block)
70
+ end
71
+
72
+ def await(tasks:, options: {}, &block)
73
+ combined = { timeout: 5 }.merge(options)
74
+ waiting = SpecWaiting.new(tasks)
75
+ ::Timeout::timeout(combined[:timeout]) do
76
+ CrossSpec.listen! do |message|
77
+ waiting.process(message, &block)
78
+ return waiting.data.length == 1 ? waiting.data.first : waiting.data if waiting.done?
79
+ nil # nil keeps blocking listen!
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,18 @@
1
+ module CrossSpec
2
+ class TaskMessage < Message
3
+ def initialize(task:, data: nil, biomarkers: nil)
4
+ super(data: data, biomarkers: biomarkers)
5
+ @task = task
6
+ end
7
+
8
+ attr_reader :task
9
+
10
+ def self.from_json(json)
11
+ new(
12
+ task: json["task"],
13
+ data: json["data"],
14
+ biomarkers: json["biomarkers"]
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module CrossSpec
2
+ VERSION = "0.1.0"
3
+ end
data/lib/cross_spec.rb ADDED
@@ -0,0 +1,59 @@
1
+ require "cross_spec/version"
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+ Bundler.setup
5
+ require "distributed_tracing"
6
+ require "json"
7
+ require "kafka"
8
+ require "securerandom"
9
+ require "timeout"
10
+
11
+ module CrossSpec
12
+ class FailureBeacon < StandardError; end
13
+ FAIL_TASK = "cross_spec:failure".freeze
14
+
15
+ autoload :Beacon, "cross_spec/beacon"
16
+ autoload :Client, "cross_spec/client"
17
+ autoload :Config, "cross_spec/config"
18
+ autoload :KafkaClient, "cross_spec/kafka_client"
19
+ autoload :Message, "cross_spec/message"
20
+ autoload :Spec, "cross_spec/spec"
21
+ autoload :SpecWaiting, "cross_spec/spec"
22
+ autoload :ServiceMessage, "cross_spec/service_message"
23
+ autoload :TaskMessage, "cross_spec/task_message"
24
+
25
+ def self.configure
26
+ @config = Config.new
27
+ yield(@config)
28
+ end
29
+
30
+ def self.config
31
+ return @config if defined?(@config)
32
+ @config = Config.new
33
+ @config
34
+ end
35
+
36
+ def self.handle(service, &block)
37
+ config.handlers[service] = Proc.new do |*args|
38
+ block.call(*args)
39
+ nil
40
+ end
41
+ end
42
+
43
+ def self.listen!
44
+ config.client.listen!
45
+ end
46
+
47
+ def self.failure_beacon!(data = nil)
48
+ beacon(FAIL_TASK, data)
49
+ end
50
+
51
+ def self.beacon(task, data = nil, biomarkers = nil)
52
+ str = JSON.dump(
53
+ task: task,
54
+ biomarkers: biomarkers || DistributedTracing.get,
55
+ data: data
56
+ )
57
+ config.client.broadcast(str)
58
+ end
59
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cross_spec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Fischer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-04-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-kafka
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: distributed_tracing
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: A framework for doing cross app testing with support for distributed
112
+ factories, test parallelism, and verification of async events.
113
+ email:
114
+ - bfischer@doximity.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - Gemfile
121
+ - LICENSE.txt
122
+ - README.md
123
+ - Rakefile
124
+ - bin/console
125
+ - bin/setup
126
+ - cross_spec.gemspec
127
+ - lib/cross_spec.rb
128
+ - lib/cross_spec/client.rb
129
+ - lib/cross_spec/config.rb
130
+ - lib/cross_spec/kafka_client.rb
131
+ - lib/cross_spec/message.rb
132
+ - lib/cross_spec/service_message.rb
133
+ - lib/cross_spec/spec.rb
134
+ - lib/cross_spec/task_message.rb
135
+ - lib/cross_spec/version.rb
136
+ homepage: https://github.com/doximity/cross_spec_ruby
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.6.14
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: A framework for doing cross app testing with support for distributed factories,
160
+ test parallelism, and verification of async events.
161
+ test_files: []