thunderspec 0.2.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 +7 -0
- data/lib/thunderspec/database.rb +33 -0
- data/lib/thunderspec/external_operations.rb +54 -0
- data/lib/thunderspec/kinds.rb +37 -0
- data/lib/thunderspec/runner.rb +33 -0
- data/lib/thunderspec/use.rb +30 -0
- data/lib/thunderspec/version.rb +6 -0
- data/lib/thunderspec.rb +61 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 11373a2d03d20bc690f946d24aa1c15e2c3f2d1bda6e69d46f9333ff5ba06b88
|
|
4
|
+
data.tar.gz: 756b81d2c0e02f3ecda669e05bd4261897de8a73586b65b892158bf991fd05f2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f259fa5b0495fdeff38d2bdb79d321ab870f7fb752b9812f567cc7f49baeb6908f35ece572e1bf002ab4f2508e9fb5fe1b6ff49ce0ffa35775460902c0798457
|
|
7
|
+
data.tar.gz: f1196703c073e44595b850540ae2ab64d820efea66774ec99849ba3fb330bbbb08c316a30c1b37d00d0956c40db38b7ab69e44485612f34b6837ddcebad06c2a
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module ThunderSpec
|
|
5
|
+
module Database
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# The test database before the suite: created when missing, then migrated, as Rails' maintain_test_schema.
|
|
9
|
+
def prepare(chunk)
|
|
10
|
+
ActiveRecord::Migration.verbose = false
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
chunk.database.verify!
|
|
14
|
+
rescue ActiveRecord::NoDatabaseError
|
|
15
|
+
chunk.database.create!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
chunk.database.migrate!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Runs the block inside a transaction rolled back after it, as Rails' transactional tests: the pool pins one
|
|
22
|
+
# connection with a non-joinable transaction open, so a transaction the code under test opens becomes a
|
|
23
|
+
# savepoint, and every thread or fiber (async) of the example uses that connection and sees the same data.
|
|
24
|
+
def isolate
|
|
25
|
+
pool = ActiveRecord::Base.connection_pool
|
|
26
|
+
pool.pin_connection!(true)
|
|
27
|
+
|
|
28
|
+
yield
|
|
29
|
+
ensure
|
|
30
|
+
pool.unpin_connection!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module ThunderSpec
|
|
5
|
+
# External operations of other chunks in a spec. No example reaches another chunk: every external operation the
|
|
6
|
+
# chunk uses answers a success Response with no payload and records its calls (an RSpec spy). `returning` changes
|
|
7
|
+
# the answer when the example needs a payload or a failure.
|
|
8
|
+
#
|
|
9
|
+
# let(:create_notification) { external_operation('notifications/notifications.create') }
|
|
10
|
+
# let(:create_notification) { external_operation('notifications/notifications.create').returning(payload: { id: 1 }) }
|
|
11
|
+
# let(:create_notification) { external_operation('notifications/notifications.create').returning(failure: 'boom') }
|
|
12
|
+
#
|
|
13
|
+
# expect(create_notification).to have_been_called.with(title: 'New Order Created', body: ':D')
|
|
14
|
+
module ExternalOperations
|
|
15
|
+
# Added to each remote while specs run.
|
|
16
|
+
module Returning
|
|
17
|
+
# The answer of every call from now on in this example: success with payload, or a failure whose message is
|
|
18
|
+
# failure (call! raises on it, as against the real chunk). Returns the remote, to keep using it as the spy.
|
|
19
|
+
def returning(payload: nil, failure: nil)
|
|
20
|
+
RSpec::Mocks.allow_message(self, :call).and_return(ExternalOperations.response(payload, failure))
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The remote the chunk's operations call for this path (`use :external_operation, path`).
|
|
26
|
+
def external_operation(path)
|
|
27
|
+
importer = ThunderSpec.chunk.external_importer.get(:external_operation)
|
|
28
|
+
importer&.items&.fetch(path, nil) ||
|
|
29
|
+
raise(Error, "no operation in chunk #{ThunderSpec.chunk.identifier} uses external operation #{path}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# have_received(:call): .with, .once, .times and not_to work as in RSpec.
|
|
33
|
+
def have_been_called = have_received(:call)
|
|
34
|
+
|
|
35
|
+
def self.response(payload, failure)
|
|
36
|
+
error = if failure
|
|
37
|
+
ThunderChunk::Operations::Remote::RemoteResponseFailure.new(
|
|
38
|
+
failure, error_class: 'ThunderOperation::Exceptions::OperationError', extras: {}, trail: []
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ThunderOperation::Response.new(payload:, was_success: failure.nil?, error:, operation_name: nil)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Before each example: every external operation of the chunk answers success, with no payload.
|
|
46
|
+
def self.answer_success
|
|
47
|
+
importer = ThunderSpec.chunk.external_importer.get(:external_operation)
|
|
48
|
+
importer&.items&.each_value do |remote|
|
|
49
|
+
remote.extend(Returning) unless remote.is_a?(Returning)
|
|
50
|
+
remote.returning
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module ThunderSpec
|
|
5
|
+
# Where the chunk keeps each kind a spec can name: internal operations and models in the registry, external
|
|
6
|
+
# operations and subscribers in the exporters the runner boots, events in the events importer.
|
|
7
|
+
module Kinds
|
|
8
|
+
LOOKUPS = {
|
|
9
|
+
operation: ->(chunk, key) { chunk.registry.get!(:operation, key) },
|
|
10
|
+
model: ->(chunk, key) { chunk.registry.get!(:model, key) },
|
|
11
|
+
external_operation: ->(chunk, key) { chunk.exporter.get(:operation).get!(key) },
|
|
12
|
+
subscriber: ->(chunk, key) { chunk.exporter.get(:event).get!(key) },
|
|
13
|
+
event: ->(chunk, key) { chunk.external_importer.get(:event).items[key] || raise(Error, "event #{key} is not registered") }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.resolve(chunk, kind, key)
|
|
17
|
+
lookup = LOOKUPS[kind.to_sym] || raise(Error, "unknown kind #{kind.inspect}; kinds: #{LOOKUPS.keys.join(', ')}")
|
|
18
|
+
|
|
19
|
+
lookup.call(chunk, key)
|
|
20
|
+
rescue ThunderChunk::Error
|
|
21
|
+
raise Error, missing(chunk, kind, key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# "no external_operation rainfall.in_date_range in chunk weather; it is an operation (ThunderSpec.rspec
|
|
25
|
+
# :operation, 'rainfall.in_date_range')", when the key is there under another kind.
|
|
26
|
+
def self.missing(chunk, kind, key)
|
|
27
|
+
found = (LOOKUPS.keys - [kind.to_sym]).find do |other|
|
|
28
|
+
LOOKUPS[other].call(chunk, key)
|
|
29
|
+
rescue ThunderChunk::Error, Error, NoMethodError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
message = "no #{kind} #{key} in chunk #{chunk.identifier}"
|
|
34
|
+
found ? "#{message}; it is an #{found} (ThunderSpec.rspec :#{found}, '#{key}')" : message
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module ThunderSpec
|
|
5
|
+
# Runs the specs of one chunk in this process: test environment, the chunk booted with its exporters, the
|
|
6
|
+
# database prepared, then RSpec over the given paths (the chunk's spec/ when none). Returns RSpec's exit code.
|
|
7
|
+
class Runner
|
|
8
|
+
EXPORTERS = [ThunderChunk::Exporters::Operations, ThunderChunk::Exporters::Events].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(chunk, paths = [], out: $stdout, err: $stderr)
|
|
11
|
+
@chunk = chunk
|
|
12
|
+
@paths = paths.empty? ? [chunk.root_path.join('spec').to_s] : paths.map(&:to_s)
|
|
13
|
+
@out = out
|
|
14
|
+
@err = err
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
ENV['THUNDER_ENV'] = 'test'
|
|
19
|
+
# ActiveRecord's database tasks print "Created database ..." unless VERBOSE is false.
|
|
20
|
+
ENV['VERBOSE'] ||= 'false'
|
|
21
|
+
# The console gem (async) logs nothing in specs: 5 is above fatal. CONSOLE_LEVEL=info turns it on.
|
|
22
|
+
ENV['CONSOLE_LEVEL'] ||= '5'
|
|
23
|
+
|
|
24
|
+
@chunk.boot(boot: { exporters: EXPORTERS })
|
|
25
|
+
Database.prepare(@chunk)
|
|
26
|
+
|
|
27
|
+
ThunderSpec.chunk = @chunk
|
|
28
|
+
ThunderSpec.configure
|
|
29
|
+
|
|
30
|
+
RSpec::Core::Runner.run(@paths, @err, @out)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module ThunderSpec
|
|
5
|
+
# `use` and `use!` in a spec, as in the console: the classes of the chunk by kind and key.
|
|
6
|
+
#
|
|
7
|
+
# ThunderSpec.rspec :operation, 'orders.create' do
|
|
8
|
+
# use :model, 'supplier' # Supplier in every example of the group
|
|
9
|
+
# let(:orders) { use!(:model, 'order') } # the class, no constant
|
|
10
|
+
module Use
|
|
11
|
+
# Group level. The key's path becomes the constant ('supplier' -> Supplier, 'client.user' -> Client::User),
|
|
12
|
+
# stubbed around each example of the group (stub_const), so nothing is left on Object after it. The key is
|
|
13
|
+
# resolved when the file loads: one the chunk does not have fails there.
|
|
14
|
+
module Group
|
|
15
|
+
def use(kind, key)
|
|
16
|
+
klass = Kinds.resolve(ThunderSpec.chunk, kind, key)
|
|
17
|
+
name = Use.constant_name(key)
|
|
18
|
+
|
|
19
|
+
before { stub_const(name, klass) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Example level: the class, without a constant.
|
|
24
|
+
module Example
|
|
25
|
+
def use!(kind, key) = Kinds.resolve(ThunderSpec.chunk, kind, key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.constant_name(key) = key.to_s.split(%r{[./]}).map(&:camelize).join('::')
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/thunderspec.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
require 'rspec'
|
|
5
|
+
require 'webmock/rspec'
|
|
6
|
+
require 'thunderchunk'
|
|
7
|
+
require 'thunderchunk/exporters/operations'
|
|
8
|
+
require 'thunderchunk/exporters/events'
|
|
9
|
+
|
|
10
|
+
require_relative 'thunderspec/version'
|
|
11
|
+
require_relative 'thunderspec/kinds'
|
|
12
|
+
require_relative 'thunderspec/use'
|
|
13
|
+
require_relative 'thunderspec/external_operations'
|
|
14
|
+
require_relative 'thunderspec/database'
|
|
15
|
+
require_relative 'thunderspec/runner'
|
|
16
|
+
|
|
17
|
+
# The specs of one chunk, on RSpec. The runner boots the chunk in the test environment, prepares its database and
|
|
18
|
+
# runs RSpec; each spec file opens with ThunderSpec.rspec, naming what it tests by its kind and key.
|
|
19
|
+
module ThunderSpec
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# The chunk under test, booted by the runner. A process tests one chunk, as it runs one.
|
|
24
|
+
attr_accessor :chunk
|
|
25
|
+
|
|
26
|
+
# ThunderSpec.rspec :operation, 'rainfall.in_date_range' do ... end
|
|
27
|
+
# An RSpec example group whose described_class is the class the chunk holds under that kind and key. A key the
|
|
28
|
+
# chunk does not have raises when the file loads.
|
|
29
|
+
def rspec(kind, key, &)
|
|
30
|
+
raise Error, 'no chunk booted; run the specs with `thunder spec`' unless chunk
|
|
31
|
+
|
|
32
|
+
RSpec.describe(Kinds.resolve(chunk, kind, key), thunder: { kind: kind.to_sym, key: }, &)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configure
|
|
36
|
+
RSpec.configure do |config|
|
|
37
|
+
config.disable_monkey_patching!
|
|
38
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
39
|
+
config.order = :random
|
|
40
|
+
|
|
41
|
+
# A group of a spec file, under any name (Params, Flow, Returns...): describe with the scope in its metadata.
|
|
42
|
+
config.alias_example_group_to :scope, scope: true
|
|
43
|
+
|
|
44
|
+
config.extend Use::Group
|
|
45
|
+
config.include Use::Example
|
|
46
|
+
config.include ExternalOperations
|
|
47
|
+
|
|
48
|
+
config.before { ExternalOperations.answer_success }
|
|
49
|
+
|
|
50
|
+
# Every example runs in a transaction rolled back after it; code that opens its own transaction gets a
|
|
51
|
+
# savepoint inside this one.
|
|
52
|
+
config.around do |example|
|
|
53
|
+
Database.isolate { example.run }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# No example reaches the network: a request nobody stubbed fails with the stub it is missing.
|
|
58
|
+
WebMock.disable_net_connect!
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: thunderspec
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Albert Alef
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.13'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: thunderchunk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.2.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.2.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webmock
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.25'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.25'
|
|
54
|
+
executables: []
|
|
55
|
+
extensions: []
|
|
56
|
+
extra_rdoc_files: []
|
|
57
|
+
files:
|
|
58
|
+
- lib/thunderspec.rb
|
|
59
|
+
- lib/thunderspec/database.rb
|
|
60
|
+
- lib/thunderspec/external_operations.rb
|
|
61
|
+
- lib/thunderspec/kinds.rb
|
|
62
|
+
- lib/thunderspec/runner.rb
|
|
63
|
+
- lib/thunderspec/use.rb
|
|
64
|
+
- lib/thunderspec/version.rb
|
|
65
|
+
licenses: []
|
|
66
|
+
metadata:
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.4'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.6.9
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: Specs of a chunk of the thunderbolt ecosystem, on RSpec
|
|
85
|
+
test_files: []
|