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 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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WARN: AI MADE
4
+ module ThunderSpec
5
+ VERSION = '0.2.0'
6
+ end
@@ -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: []