memoria 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require 'memoria/configuration'
2
+ require 'memoria/snapshot'
3
+ require 'memoria/snapshot_saver'
4
+ require 'memoria/version'
5
+
6
+ # Encapsulates and provides access to the public interface of the gem.
7
+ module Memoria
8
+ extend self
9
+
10
+ # The last recorded snapshot.
11
+ #
12
+ # @example
13
+ # Memoria.record('name-of-snapshot')
14
+ # Memoria.current_snapshot # => #<Memoria::Snapshot:0x00007fc5610a17b0 @name="name-of-snapshot">
15
+ #
16
+ # @return [Snapshot] The current snapshot or nil if there is no current snapshot.
17
+ #
18
+ # @api public
19
+ #
20
+ def current_snapshot
21
+ snapshots.last
22
+ end
23
+
24
+ # Configures Memoria.
25
+ #
26
+ # @example
27
+ # Memoria.configure do |config|
28
+ # config.snapshot_extension = 'snappy'
29
+ # config.snapshot_record_mode = :new_episodes
30
+ # config.add_setting :custom_setting_name do
31
+ # Time.now
32
+ # end
33
+ # end
34
+ #
35
+ # @yield The configuration block.
36
+ # @yieldparam config [Configuration] The configuration object.
37
+ # @return [void]
38
+ #
39
+ # @see Memoria::Configuration The configuration object and its available configuration options.
40
+ #
41
+ # @api public
42
+ #
43
+ def configure
44
+ yield configuration
45
+ end
46
+
47
+ # Exposes the gem's configuration.
48
+ #
49
+ # @example
50
+ # Memoria.configuration.snapshot_extension # => 'snap'
51
+ #
52
+ # @return [Configuration] The Memoria configuration.
53
+ #
54
+ # @api public
55
+ #
56
+ def configuration
57
+ @configuration ||= Configuration.new
58
+ end
59
+
60
+ # Returns the list of recorded snapshots.
61
+ #
62
+ # @example
63
+ # Memoria.record('name-of-snapshot-one')
64
+ # Memoria.snapshots # => [#<Memoria::Snapshot:0x00007fc7a3870808 @name="name-of-snapshot-one">]
65
+ #
66
+ # @return [Array<Snapshot>] An array of recorded snapshots.
67
+ #
68
+ # @api public
69
+ #
70
+ def snapshots
71
+ @snapshots ||= []
72
+ end
73
+
74
+ # Returns an entity to persist snapshots (on the file system).
75
+ #
76
+ # @example
77
+ # snapshot_saver = Memoria.snapshot_saver
78
+ # snapshot_saver.save(Snapshot.new('index-page'))
79
+ #
80
+ # @return [SnapshotSaver] Reads and writes snapshots to the disk.
81
+ #
82
+ # @api public
83
+ #
84
+ def snapshot_saver
85
+ @snapshot_saver ||= SnapshotSaver.new(configuration)
86
+ end
87
+
88
+ # Records a new snapshot.
89
+ #
90
+ # @example
91
+ # Memoria.record('name-of-snapshot')
92
+ # Memoria.current_snapshot # => #<Memoria::Snapshot:0x00007fc5610a17b0 @name="name-of-snapshot">
93
+ #
94
+ # @param [String] snapshot_name The name of the snapshot.
95
+ # @return [Snapshot] A new snapshot.
96
+ #
97
+ # @api public
98
+ #
99
+ def record(snapshot_name)
100
+ snapshot = Snapshot.new(snapshot_name)
101
+ snapshots.push(snapshot)
102
+ end
103
+
104
+ # Stops recording snapshots.
105
+ #
106
+ # @example
107
+ # Memoria.record('name-of-snapshot')
108
+ # Memoria.current_snapshot # => #<Memoria::Snapshot:0x00007fc5610a17b0 @name="name-of-snapshot">
109
+ # Memoria.stop_recording
110
+ # Memoria.current_snapshot # => nil
111
+ #
112
+ # @return [Array] An empty array.
113
+ #
114
+ # @api public
115
+ #
116
+ def stop_recording
117
+ snapshots.clear
118
+ end
119
+ end
@@ -0,0 +1,112 @@
1
+ require 'memoria/errors'
2
+
3
+ module Memoria
4
+ # Stores the configuration of the gem.
5
+ class Configuration
6
+ # The only allowed snapshot record modes.
7
+ VALID_SNAPSHOT_RECORD_MODES = %i[all new_episodes none].freeze
8
+
9
+ # @return [String] Directory where the snapshots will be saved
10
+ # @api public
11
+ #
12
+ attr_accessor :snapshot_directory
13
+
14
+ # @return [String] The way the snapshots are recorded.
15
+ # @api public
16
+ #
17
+ attr_reader :snapshot_record_mode
18
+
19
+ # @return [String] File extension of new snapshots.
20
+ # @api public
21
+ #
22
+ attr_reader :snapshot_extension
23
+
24
+ # Creates an instance of the configuration.
25
+ #
26
+ # @example
27
+ # configuration = Memoria::Configuration.new
28
+ #
29
+ # @return [Configuration] An instance of +Configuration+.
30
+ #
31
+ # @api public
32
+ #
33
+ def initialize
34
+ self.snapshot_extension = 'snap'
35
+ self.snapshot_record_mode = :new_episodes
36
+ end
37
+
38
+ # Sets the extension for new snapshots.
39
+ #
40
+ # @param [Symbol] snapshot_extension The new snapshot extension.
41
+ # @raise [Errors::InvalidSnapshotExtension] If the snapshot extension is not valid.
42
+ #
43
+ # @return [String] The given snapshot extension.
44
+ #
45
+ # @api public
46
+ #
47
+ def snapshot_extension=(snapshot_extension)
48
+ validate_snapshot_extension(snapshot_extension)
49
+ @snapshot_extension = snapshot_extension
50
+ end
51
+
52
+ # Sets the snapshot record mode. TODO
53
+ #
54
+ # @param [Symbol] record_mode The new snapshot record mode.
55
+ # @raise [Errors::InvalidRecordMode] If record mode is not one of +VALID_SNAPSHOT_RECORD_MODES+
56
+ #
57
+ # @return [String] The given snapshot extension.
58
+ #
59
+ # @api public
60
+ #
61
+ def snapshot_record_mode=(record_mode)
62
+ validate_record_mode(record_mode)
63
+ @snapshot_record_mode = record_mode
64
+ end
65
+
66
+ # Adds a new setting with the given +name+.
67
+ #
68
+ # @example Configuring a setting directly.
69
+ # configuration = Memoria::Configuration
70
+ # configuration.add_setting(:now) do
71
+ # Time.now
72
+ # end
73
+ #
74
+ # @example Configuring a setting through Memoria's DSL.
75
+ # Memoria.configure do |config|
76
+ # config.add_setting(:now) do
77
+ # Time.now
78
+ # end
79
+ # end
80
+ #
81
+ # @param [Symbol] name The name of the setting.
82
+ # @param [Symbol] block The block to be executed when the setting is called.
83
+ # @raise [Errors::DuplicateSetting] If there is already a setting defined with that name.
84
+ #
85
+ # @return [Symbol] The name of the setting
86
+ #
87
+ # @api public
88
+ #
89
+ def add_setting(name, &block)
90
+ validate_setting_existence(name)
91
+ define_singleton_method(name, block)
92
+ end
93
+
94
+ private
95
+
96
+ # Checks if there is a setting already defined with the given +setting_name.
97
+ def validate_setting_existence(setting_name)
98
+ error_message = "There is already a setting defined with the name #{setting_name}"
99
+ raise Errors::DuplicateSetting, error_message if respond_to?(setting_name)
100
+ end
101
+
102
+ # Checks if the snapshot record mode is one of the modes declared in +VALID_SNAPSHOT_RECORD_MODES+.
103
+ def validate_record_mode(record_mode)
104
+ raise Errors::InvalidRecordMode unless VALID_SNAPSHOT_RECORD_MODES.include?(record_mode.to_sym)
105
+ end
106
+
107
+ # Checks if the snapshot extension is valid.
108
+ def validate_snapshot_extension(snapshot_extension)
109
+ raise Errors::InvalidSnapshotExtension unless snapshot_extension =~ /^[a-zA-Z0-9]+$/
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,4 @@
1
+ require 'memoria/errors/invalid_configuration'
2
+ require 'memoria/errors/invalid_record_mode'
3
+ require 'memoria/errors/invalid_snapshot_extension'
4
+ require 'memoria/errors/duplicate_setting'
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when attempting to add a setting that already exists.
4
+ class DuplicateSetting < InvalidConfiguration
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ "There's already a setting with that name."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when the configuration is invalid. This is a parent class to all the other classes.
4
+ class InvalidConfiguration < RuntimeError
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ "Memoria's configuration is invalid."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when the record mode isn't one of +all+, +none+ or +new_snapshots+.
4
+ class InvalidRecordMode < InvalidConfiguration
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ 'The snapshot record mode is invalid. The only valid modes are :all, :none and :new_snapshots.'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when the snapshot extension doesn't follow a file naming pattern.
4
+ class InvalidSnapshotExtension < InvalidConfiguration
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ 'The snapshot extension is invalid.'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require 'memoria'
2
+ require 'memoria/rspec/configurator'
3
+
4
+ Memoria.configure do |config|
5
+ config.add_setting(:configure_rspec_hooks) do
6
+ Memoria::RSpec::Configurator.configure_rspec_hooks
7
+ end
8
+
9
+ config.add_setting(:include_rspec_matchers) do
10
+ Memoria::RSpec::Configurator.include_rspec_matchers
11
+ end
12
+
13
+ config.snapshot_directory = 'spec/fixtures/snapshots' unless config.snapshot_directory.nil?
14
+ end
@@ -0,0 +1,46 @@
1
+ require 'memoria/rspec/metadata'
2
+ require 'memoria/rspec/metadata_parser'
3
+
4
+ module Memoria
5
+ module RSpec
6
+ # Configures the integration with RSpec.
7
+ module Configurator
8
+ module_function
9
+
10
+ # Configures RSpec's +before+ and +after+ hooks to record snapshots when +match_snapshot+ is called.
11
+ #
12
+ # @return [void]
13
+ #
14
+ # @api private
15
+ #
16
+ def configure_rspec_hooks
17
+ ::RSpec.configure do |config|
18
+ config.before(:each, snapshot: true) do |example|
19
+ current_example = example.respond_to?(:metadata) ? example : example.example
20
+ snapshot_name = Memoria::RSpec::MetadataParser.find_description_for(current_example.metadata)
21
+
22
+ Memoria.record(snapshot_name)
23
+ end
24
+
25
+ config.after(:each, snapshot: true) do
26
+ Memoria.stop_recording
27
+ end
28
+ end
29
+ end
30
+
31
+ # Includes RSpec's matchers such as +match_snapshot+.
32
+ #
33
+ # @return [void]
34
+ #
35
+ # @api private
36
+ #
37
+ def include_rspec_matchers
38
+ require 'memoria/rspec/matcher'
39
+
40
+ ::RSpec.configure do |config|
41
+ config.include Memoria::RSpec::Metadata
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec::Matchers.define :match_snapshot do
4
+ diffable
5
+
6
+ match do |actual|
7
+ snapshot_name = Memoria.current_snapshot.name
8
+
9
+ if snapshot_saver.snapshot_exists?(snapshot_name)
10
+ @expected = snapshot_saver.read(snapshot_name)
11
+ expect(expected).to eq(actual)
12
+ else
13
+ snapshot_saver.write(snapshot_name, actual)
14
+ RSpec.configuration.reporter.message "Generated snapshot: #{snapshot_name}"
15
+ true
16
+ end
17
+ end
18
+
19
+ # :nocov:
20
+ failure_message do
21
+ 'Received value does not match the stored snapshot'
22
+ end
23
+ # :nocov:
24
+
25
+ def snapshot_saver
26
+ Memoria.snapshot_saver
27
+ end
28
+
29
+ attr_reader :expected
30
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module RSpec
3
+ # Provides methods to retrieve RSpec's example metadata
4
+ module Metadata
5
+ # Retrieve the metadata from the current example
6
+ #
7
+ # @api private
8
+ #
9
+ # @return [Hash] RSpec's metadata of the current example.
10
+ #
11
+ def current_example_metadata
12
+ self.class.metadata
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ module Memoria
2
+ module RSpec
3
+ # Extracts information from RSpec's examples metadata.
4
+ module MetadataParser
5
+ extend self
6
+
7
+ # Finds RSpec's description of a given example's metadata.
8
+ #
9
+ # @param [Hash] metadata RSpec's metadata of a given example.
10
+ #
11
+ # @return [String]
12
+ #
13
+ # @api private
14
+ #
15
+ def find_description_for(metadata)
16
+ description = find_description_in(metadata)
17
+ example_group = find_example_group_in(metadata)
18
+
19
+ if example_group
20
+ [find_description_for(example_group), description].join('/')
21
+ else
22
+ description
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # Finds an RSpec description in a given metadata.
29
+ #
30
+ # @param [Hash] metadata RSpec's metadata
31
+ #
32
+ # @return [String] The description of an RSpec example
33
+ #
34
+ def find_description_in(metadata)
35
+ metadata[:description].empty? ? metadata[:scoped_id] : metadata[:description]
36
+ end
37
+
38
+ # Finds an RSpec example group in a given metadata.
39
+ #
40
+ # @param [Hash] metadata RSpec's metadata
41
+ #
42
+ # @return [Hash] The metadata of an RSpec example group
43
+ #
44
+ def find_example_group_in(metadata)
45
+ metadata.key?(:example_group) ? metadata[:example_group] : metadata[:parent_example_group]
46
+ end
47
+ end
48
+ end
49
+ end