super8 0.2.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 300247513d3de06e1d28c93a61d7d2e4f499004aafac7bbcff91ad75faf5f7d6
4
+ data.tar.gz: 6ee5b1c1729277708ee73248413ec604cc7224ec3d342213fc75fd3ec92748a8
5
+ SHA512:
6
+ metadata.gz: 58f16618e726f6d1d665839a85f63a6864a39899b81f2d31139d69ff47545a3a3a5261dc1f73d955fa080b350454d6c7bf21046ec9713fd1a262dc589dff53b1
7
+ data.tar.gz: 407ad63d77e0ad4e60f87209d4e0b36c51003ecf951d874e66ecbadadbff77b8f6714579ba1e81964df53a770a560438650ec21d2780bfb24380570fc511c572
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 John Cipriano
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Super 8 🎞️
2
+
3
+ Record and replay ruby-odbc sessions for easier, more accurate testing.
4
+
5
+ Super 8 is a "vcr"-like library for recording and playing back ODBC interactions made with ruby-odbc, for use with automated testing. More accurate than manually mocking and less work than adding an ODBC-enabled server to your test stack.
6
+
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem "super8", github: "johncip/super8", branch: "main"
14
+ ```
15
+
16
+ Then execute:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+
23
+ ## Requirements
24
+
25
+ - Ruby 3.1 or higher
26
+ - ruby-odbc gem
27
+
28
+ I haven't tested on lower Ruby versions, but it may work.
29
+ * It uses YAML from stdlib, which I think sets hard minimum of 1.8.
30
+ * It uses CSV from stdlib, so for Ruby 3.4+ you will need to add `gem "csv"`.
31
+
32
+
33
+ ## Configuration
34
+
35
+ Configure Super8 in your test setup (e.g., `spec/spec_helper.rb`):
36
+
37
+ ```ruby
38
+ require "super8"
39
+
40
+ Super8.configure do |config|
41
+ # defaults to "spec/super8_cassettes"
42
+ config.cassette_directory = "spec/super8_cassettes"
43
+
44
+ # for writing cassettes, defaults to "utf8"
45
+ config.encoding = "ibm437"
46
+ end
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ Use cassettes in your tests with `use_cassette`:
52
+
53
+ ```ruby
54
+ RSpec.describe "ODBC stuff" do
55
+ it "queries the database" do
56
+ Super8.use_cassette("users/all", mode: :playback) do
57
+ db = ODBC.connect("dsn")
58
+ result = db.run("SELECT * FROM users")
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ Modes:
65
+ - `:record` - Records ODBC interactions to a cassette file
66
+ - `:playback` - Replays recorded interactions without connecting to the database
67
+
68
+
69
+ ## Supported Methods
70
+
71
+ Currently supported ODBC methods:
72
+ - `ODBC` : `.connect`
73
+ - `ODBC::Database`: `#run`
74
+ - `ODBC::Statement`: `#columns`, `#fetch_all`, `#drop`, `#cancel`, `#close`
75
+
76
+
77
+ ## Motivation
78
+
79
+ I wrote and maintain an application that communicates with the [Retalix](https://en.wikipedia.org/wiki/Retalix) ERP. I use [ruby-odbc](https://github.com/larskanis/ruby-odbc) to connect to the instance and incrementally sync a number of tables.
80
+
81
+ In testing, I would stub the ODBC parts out, which was low-level and noisy. It also meant I had to check the queries for regressions manually.
82
+
83
+ **vcr** is a library for "recording" HTTP API requests, so that they can be accurately mocked in ruby tests. It stores the request as well as the response, so that if the request made changes, it can be considered a test failure. It's also very easy to record "cassettes." I've wanted something like that for for ODBC.
84
+
85
+
86
+ ## Development
87
+
88
+ This project was developed with AI assistance as an experiment in improving and documenting my workflow. The process and decisions are documented in the [design directory](design/).
89
+
90
+ * [Design docs](design/)
91
+ * [Copilot chat logs](design/copilot_chat_logs/)
92
+ * [Architecture decision records](design/decision_records/)
93
+
94
+ ## License
95
+
96
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -0,0 +1,119 @@
1
+ require "yaml"
2
+ require "csv"
3
+
4
+ module Super8
5
+ # Represents a recorded cassette stored on disk.
6
+ # Each cassette is a directory containing commands and row data.
7
+ class Cassette
8
+ SCHEMA_VERSION = 3
9
+
10
+ attr_reader :name
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ @current_connection_id = nil
15
+ @connections = {}
16
+ @commands = []
17
+ @command_index = 0
18
+ end
19
+
20
+ # Full path to the cassette directory.
21
+ def path
22
+ File.join(Super8.config.cassette_directory, name)
23
+ end
24
+
25
+ def exists?
26
+ Dir.exist?(path)
27
+ end
28
+
29
+ # Generate next connection ID (a, b, c, ..., z, aa, ab, ...)
30
+ def next_connection_id
31
+ return @current_connection_id = "a" if @current_connection_id.nil?
32
+ @current_connection_id = @current_connection_id.succ
33
+ end
34
+
35
+ # Writes all cassette data to disk in one pass.
36
+ # Creates directory, row files, commands.yml, and metadata.yml.
37
+ def save
38
+ FileUtils.mkdir_p(path)
39
+ process_row_data!
40
+ save_metadata_file
41
+ save_commands_file
42
+ end
43
+
44
+ # Loads cassette data for playback.
45
+ # Raises CassetteNotFoundError if missing.
46
+ def load
47
+ raise CassetteNotFoundError, "Cassette not found: #{path}" unless exists?
48
+ @commands = YAML.load_file(
49
+ File.join(path, "commands.yml"),
50
+ permitted_classes: [ODBC::Column]
51
+ )
52
+ @command_index = 0
53
+ end
54
+
55
+ # Get next command during playback.
56
+ def next_command
57
+ return nil if @command_index >= @commands.length
58
+ command = @commands[@command_index]
59
+ @command_index += 1
60
+ command
61
+ end
62
+
63
+ # Load row data from CSV files.
64
+ def load_rows_from_file(filename)
65
+ CSV.read(File.join(path, filename))
66
+ end
67
+
68
+ # Generic recording — stores method name and context in memory.
69
+ def record(method, **context)
70
+ @commands << {"method" => method.to_s, **stringify_keys(context)}
71
+ end
72
+
73
+ private
74
+
75
+ def stringify_keys(hash)
76
+ hash.transform_keys(&:to_s)
77
+ end
78
+
79
+ # Extracts row data from commands, writes to CSV files, replaces with file references
80
+ def process_row_data!
81
+ @commands.each do |command|
82
+ next unless command.key?("rows_data")
83
+
84
+ conn_id = command["connection_id"]
85
+ stmt_id = command["statement_id"]
86
+ method = command["method"]
87
+ file_name = "#{conn_id}_#{stmt_id}_#{method}.csv"
88
+ write_rows_csv_file(file_name, command.delete("rows_data"))
89
+ command["rows_file"] = file_name
90
+ end
91
+ end
92
+
93
+ def write_rows_csv_file(file_name, data)
94
+ file_path = File.join(path, file_name)
95
+ CSV.open(file_path, "w") do |csv|
96
+ data.each do |row|
97
+ csv << row.map do |field|
98
+ field&.encode(Super8.config.encoding, invalid: :replace, undef: :replace, replace: "?")
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def save_metadata_file
105
+ metadata_file = File.join(path, "metadata.yml")
106
+ metadata = {
107
+ "schema_version" => SCHEMA_VERSION,
108
+ "recorded_with" => Super8::VERSION,
109
+ "recorded_on" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
110
+ }
111
+ File.write(metadata_file, metadata.to_yaml)
112
+ end
113
+
114
+ def save_commands_file
115
+ commands_file = File.join(path, "commands.yml")
116
+ File.write(commands_file, @commands.to_yaml)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,12 @@
1
+ module Super8
2
+ # Configuration class for Super8 settings.
3
+ class Config
4
+ attr_accessor :cassette_directory, :record_mode, :encoding
5
+
6
+ def initialize
7
+ @cassette_directory = "spec/super8_cassettes"
8
+ @record_mode = :once
9
+ @encoding = "utf-8"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "diff/lcs"
4
+
5
+ module Super8
6
+ # Helper module for generating diffs in error messages.
7
+ module DiffHelper
8
+ # Generate a diff between expected and actual values.
9
+ def self.generate_diff(expected, actual, key: "value")
10
+ # Handle nil cases
11
+ return "#{key}: expected #{expected.inspect}, got #{actual.inspect}" if expected.nil? || actual.nil?
12
+
13
+ # For non-string values, fall back to inspect
14
+ return "#{key}: expected #{expected.inspect}, got #{actual.inspect}" unless expected.is_a?(String) && actual.is_a?(String)
15
+
16
+ expected_lines = expected.lines.map(&:chomp)
17
+ actual_lines = actual.lines.map(&:chomp)
18
+
19
+ diff = Diff::LCS.sdiff(expected_lines, actual_lines)
20
+
21
+ result = ["#{key} mismatch:"]
22
+ diff.each_with_index do |change, i|
23
+ case change.action
24
+ when "-"
25
+ result << " - #{change.old_element}"
26
+ when "+"
27
+ result << " + #{change.new_element}"
28
+ when "!"
29
+ result << " - #{change.old_element}"
30
+ result << " + #{change.new_element}"
31
+ when "="
32
+ result << " #{change.old_element}"
33
+ end
34
+ end
35
+
36
+ result.join("\n")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ module Super8
2
+ # Base error class for Super 8 exceptions.
3
+ class Error < StandardError; end
4
+
5
+ # Raised when a requested cassette is not found on disk.
6
+ class CassetteNotFoundError < Error; end
7
+
8
+ # Raised when playback interactions don't match recorded commands.
9
+ class CommandMismatchError < Error; end
10
+ end
@@ -0,0 +1,57 @@
1
+ require_relative "diff_helper"
2
+
3
+ module Super8
4
+ # Wraps ODBC calls during playback mode.
5
+ # Validates that interactions match recorded commands and returns recorded data.
6
+ class PlaybackDatabaseWrapper
7
+ def initialize(cassette, connection_id)
8
+ @cassette = cassette
9
+ @connection_id = connection_id
10
+ end
11
+
12
+ def run(sql, *params)
13
+ expected_command = @cassette.next_command
14
+ validate_command_match(expected_command, :run,
15
+ connection_id: @connection_id, sql: sql, params: params)
16
+
17
+ statement_id = expected_command["statement_id"]
18
+ PlaybackStatementWrapper.new(statement_id, @cassette, @connection_id)
19
+ end
20
+
21
+ # Delegate other methods - they shouldn't be called in typical playback scenarios
22
+ def method_missing(method_name, ...)
23
+ raise NoMethodError, "Method #{method_name} not implemented for playback"
24
+ end
25
+
26
+ def respond_to_missing?(_method_name, _include_private=false)
27
+ false
28
+ end
29
+
30
+ private
31
+
32
+ def validate_command_match(expected_command, method, **actual_context)
33
+ raise CommandMismatchError, "No more recorded interactions" if expected_command.nil?
34
+
35
+ # Check method
36
+ if expected_command["method"] != method.to_s
37
+ raise CommandMismatchError,
38
+ "method: expected '#{expected_command['method']}', got '#{method}'"
39
+ end
40
+
41
+ # Check each key/value pair
42
+ actual_context.each do |key, value|
43
+ expected_value = expected_command[key.to_s]
44
+ next if expected_value == value
45
+
46
+ # Use diff for SQL queries to show detailed differences
47
+ error_message = if key == :sql && expected_value.is_a?(String) && value.is_a?(String)
48
+ DiffHelper.generate_diff(expected_value, value, key: "sql")
49
+ else
50
+ "#{key}: expected #{expected_value.inspect}, got #{value.inspect}"
51
+ end
52
+
53
+ raise CommandMismatchError, error_message
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,80 @@
1
+ module Super8
2
+ # Wraps ODBC Statement calls during playback mode.
3
+ # Validates that statement interactions match recorded commands and returns recorded data.
4
+ class PlaybackStatementWrapper
5
+ def initialize(statement_id, cassette, connection_id)
6
+ @statement_id = statement_id
7
+ @cassette = cassette
8
+ @connection_id = connection_id
9
+ end
10
+
11
+ def fetch_all
12
+ expected_command = @cassette.next_command
13
+ validate_statement_command(expected_command, :fetch_all)
14
+
15
+ rows_file_path = expected_command["rows_file"]
16
+ rows_file_path ? @cassette.load_rows_from_file(rows_file_path) : []
17
+ end
18
+
19
+ def columns
20
+ expected_command = @cassette.next_command
21
+ validate_statement_command(expected_command, :columns)
22
+ expected_command["result"]
23
+ end
24
+
25
+ def drop
26
+ expected_command = @cassette.next_command
27
+ validate_statement_command(expected_command, :drop)
28
+ # drop doesn't return anything
29
+ end
30
+
31
+ def close
32
+ expected_command = @cassette.next_command
33
+ validate_statement_command(expected_command, :close)
34
+ # close doesn't return anything
35
+ end
36
+
37
+ def cancel
38
+ expected_command = @cassette.next_command
39
+ validate_statement_command(expected_command, :cancel)
40
+ # cancel doesn't return anything
41
+ end
42
+
43
+ # Delegate other methods - raise error for unexpected calls
44
+ def method_missing(method_name, ...)
45
+ raise NoMethodError, "Method #{method_name} not implemented for playback"
46
+ end
47
+
48
+ def respond_to_missing?(_method_name, _include_private=false)
49
+ false
50
+ end
51
+
52
+ private
53
+
54
+ def validate_statement_command(expected_command, method)
55
+ validate_command_match(expected_command, method,
56
+ connection_id: @connection_id, statement_id: @statement_id)
57
+ end
58
+
59
+ # :reek:FeatureEnvy
60
+ # :reek:NilCheck
61
+ def validate_command_match(expected_command, method, **actual_context)
62
+ raise CommandMismatchError, "No more recorded interactions" if expected_command.nil?
63
+
64
+ # Check method
65
+ if expected_command["method"] != method.to_s
66
+ raise CommandMismatchError,
67
+ "method: expected '#{expected_command['method']}', got '#{method}'"
68
+ end
69
+
70
+ # Check each key/value pair
71
+ actual_context.each do |key, value|
72
+ expected_value = expected_command[key.to_s]
73
+ if expected_value != value
74
+ raise CommandMismatchError,
75
+ "#{key}: expected #{expected_value.inspect}, got #{value.inspect}"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ module Super8
2
+ # Wraps an ODBC::Database to intercept method calls for recording.
3
+ # Delegates to the real database and logs interactions to the cassette.
4
+ class RecordingDatabaseWrapper
5
+ def initialize(real_db, cassette, connection_id)
6
+ @real_db = real_db
7
+ @cassette = cassette
8
+ @connection_id = connection_id
9
+ @statement_counter = 0
10
+ end
11
+
12
+ # Intercept run method to record SQL queries
13
+ def run(sql, *params)
14
+ statement_id = next_statement_id
15
+ @cassette.record(:run, connection_id: @connection_id,
16
+ sql: sql, params: params, statement_id: statement_id)
17
+ real_statement = @real_db.run(sql, *params)
18
+ RecordingStatementWrapper.new(real_statement, statement_id, @cassette, @connection_id)
19
+ end
20
+
21
+ # Delegate all other method calls to the real database for now.
22
+ def method_missing(method_name, ...)
23
+ @real_db.send(method_name, ...)
24
+ end
25
+
26
+ def respond_to_missing?(method_name, include_private=false)
27
+ @real_db.respond_to?(method_name, include_private) || super
28
+ end
29
+
30
+ private
31
+
32
+ def next_statement_id
33
+ @statement_counter += 1
34
+ @statement_counter
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ module Super8
2
+ # Wraps an ODBC::Statement to intercept method calls for recording.
3
+ # During record mode, delegates to the real statement and logs interactions.
4
+ class RecordingStatementWrapper
5
+ def initialize(real_statement, statement_id, cassette, connection_id)
6
+ @real_statement = real_statement
7
+ @statement_id = statement_id
8
+ @cassette = cassette
9
+ @connection_id = connection_id
10
+ end
11
+
12
+ # Intercept columns method to record column metadata
13
+ # :reek:BooleanParameter
14
+ def columns(as_ary=false) # rubocop:disable Style/OptionalBooleanParameter
15
+ result = @real_statement.columns(as_ary)
16
+ @cassette.record(:columns,
17
+ connection_id: @connection_id, statement_id: @statement_id,
18
+ as_ary: as_ary, result: result)
19
+ result
20
+ end
21
+
22
+ # Intercept fetch_all method to record row data
23
+ def fetch_all
24
+ result = @real_statement.fetch_all
25
+ # Normalize nil to empty array to match CSV playback behavior
26
+ normalized_result = result || []
27
+ @cassette.record(:fetch_all,
28
+ connection_id: @connection_id, statement_id: @statement_id,
29
+ rows_data: normalized_result)
30
+ result
31
+ end
32
+
33
+ # Intercept drop method to record cleanup call
34
+ def drop
35
+ result = @real_statement.drop
36
+ @cassette.record(:drop, connection_id: @connection_id, statement_id: @statement_id)
37
+ result
38
+ end
39
+
40
+ # Intercept cancel method to record statement cancellation
41
+ def cancel
42
+ result = @real_statement.cancel
43
+ @cassette.record(:cancel, connection_id: @connection_id, statement_id: @statement_id)
44
+ result
45
+ end
46
+
47
+ # Intercept close method to record statement closure
48
+ def close
49
+ result = @real_statement.close
50
+ @cassette.record(:close, connection_id: @connection_id, statement_id: @statement_id)
51
+ result
52
+ end
53
+
54
+ # Delegate all other method calls to the real statement for now.
55
+ # Future phases will intercept specific methods (fetch, etc.)
56
+ def method_missing(method_name, ...)
57
+ @real_statement.send(method_name, ...)
58
+ end
59
+
60
+ def respond_to_missing?(method_name, include_private=false)
61
+ @real_statement.respond_to?(method_name, include_private) || super
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super8
4
+ VERSION = "0.2.1"
5
+ end
data/lib/super8.rb ADDED
@@ -0,0 +1,100 @@
1
+ require_relative "super8/version"
2
+ require_relative "super8/config"
3
+ require_relative "super8/errors"
4
+ require_relative "super8/cassette"
5
+ require_relative "super8/recording_database_wrapper"
6
+ require_relative "super8/recording_statement_wrapper"
7
+ require_relative "super8/playback_database_wrapper"
8
+ require_relative "super8/playback_statement_wrapper"
9
+ require "odbc"
10
+
11
+ # Top-level module for Super 8.
12
+ module Super8
13
+ class << self
14
+ def config
15
+ @config ||= Config.new
16
+ end
17
+
18
+ def configure
19
+ yield config
20
+ end
21
+
22
+ # Wraps a block with cassette recording/playback.
23
+ # In record mode, ODBC calls are captured and saved.
24
+ # In playback mode, recorded responses are returned.
25
+ def use_cassette(name, mode: :record)
26
+ cassette = Cassette.new(name)
27
+
28
+ case mode
29
+ when :record
30
+ setup_recording_mode(cassette)
31
+ when :playback
32
+ setup_playback_mode(cassette)
33
+ else
34
+ raise ArgumentError, "Unknown mode: #{mode}. Use :record or :playback"
35
+ end
36
+
37
+ yield
38
+ ensure
39
+ cleanup_odbc_intercept
40
+ cassette.save if mode == :record
41
+ end
42
+
43
+ private
44
+
45
+ def setup_recording_mode(cassette)
46
+ original_connect = ODBC.method(:connect)
47
+ @original_connect = original_connect
48
+
49
+ ODBC.define_singleton_method(:connect) do |dsn, &block|
50
+ # Generate connection ID for this connection
51
+ connection_id = cassette.next_connection_id
52
+
53
+ # Record connect command with connection metadata
54
+ cassette.record(:connect, connection_id: connection_id, dsn: dsn)
55
+
56
+ original_connect.call(dsn) do |db|
57
+ wrapped_db = RecordingDatabaseWrapper.new(db, cassette, connection_id)
58
+ block&.call(wrapped_db)
59
+ end
60
+ end
61
+ end
62
+
63
+ def setup_playback_mode(cassette)
64
+ cassette.load # Load cassette data
65
+
66
+ original_connect = ODBC.method(:connect)
67
+ @original_connect = original_connect
68
+
69
+ ODBC.define_singleton_method(:connect) do |dsn, &block|
70
+ # Get next connect command from cassette
71
+ expected_command = cassette.next_command
72
+ raise CommandMismatchError, "No more recorded interactions" if expected_command.nil?
73
+
74
+ # Validate it's a connect command
75
+ unless expected_command["method"] == "connect"
76
+ raise CommandMismatchError,
77
+ "method: expected 'connect', got '#{expected_command['method']}'"
78
+ end
79
+
80
+ # Validate DSN matches
81
+ recorded_dsn = expected_command["dsn"]
82
+ unless dsn == recorded_dsn
83
+ raise CommandMismatchError, "dsn: expected '#{recorded_dsn}', got '#{dsn}'"
84
+ end
85
+
86
+ # Extract connection ID
87
+ connection_id = expected_command["connection_id"]
88
+
89
+ # Return fake database object
90
+ playback_db = PlaybackDatabaseWrapper.new(cassette, connection_id)
91
+ block&.call(playback_db)
92
+ end
93
+ end
94
+
95
+ def cleanup_odbc_intercept
96
+ ODBC.define_singleton_method(:connect, @original_connect) if @original_connect
97
+ @original_connect = nil
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,22 @@
1
+ require "bundler/setup"
2
+ require "yaml"
3
+ require "fileutils"
4
+ require "fakefs/spec_helpers"
5
+
6
+ require "super8"
7
+
8
+ RSpec.configure do |config|
9
+ config.expect_with :rspec do |expectations|
10
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
11
+ end
12
+
13
+ config.mock_with :rspec do |mocks|
14
+ mocks.verify_partial_doubles = true
15
+ end
16
+
17
+ config.shared_context_metadata_behavior = :apply_to_host_groups
18
+ config.filter_run_when_matching :focus
19
+ config.disable_monkey_patching!
20
+ config.order = :random
21
+ Kernel.srand config.seed
22
+ end