schema-test 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 260d4172bc31999c0e327e3ac45428d042b7977bbad68c5b5d77e9eadba6765d
4
- data.tar.gz: ff9ff5f54b598950d61b955750473007bd66afafeaa0d81cbb937a877c460c36
3
+ metadata.gz: 63c3ad67322093fabc506bf99f0f8677eb24b929d7f17bb569656d1951bc6e71
4
+ data.tar.gz: 3ef25c82500f915aeab5b522514778742a5235e4ae9289b1178f07be4faf998d
5
5
  SHA512:
6
- metadata.gz: 6592e1e7f3b5d0f6ddb2fdfe5099d87de1e2674d3abb5e7a3e3268537ff86ff0bccdf933c4fdf563e581c54b5ec11025db9b16fa05477cf3c24783813e7667a2
7
- data.tar.gz: 9d295a52695e323afe73236219694d4e51ea14754e323464f48328bf9421362626bc522ac286211e91a207858b6de25573272949c4e1917f9ae68e24b024f561
6
+ metadata.gz: 4a3cc4befe4ecd6fec9f948ed6c9048306948a530b815a1dab4c2d127d4557b1a0e6d62ae61bd7ffcf4e7ea622a27fc3151ad4c0209a6cd8d1d15abe7a470586
7
+ data.tar.gz: f2dd3828fc7ce1ae14df985408d08e64444bd4efd406b71275daa49889c21caf7d78703f2490cfe8b4e6f1f1d3f9bb737f5d6c1cf54e6524ef1cd831a953a6f4
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "bundler"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -19,15 +19,12 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby-version: ['2.6', '2.7', '3.0']
22
+ ruby-version: ['3.1', '3.2', '3.3']
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
26
26
  - name: Set up Ruby
27
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
- # uses: ruby/setup-ruby@v1
30
- uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
27
+ uses: ruby/setup-ruby@v1
31
28
  with:
32
29
  ruby-version: ${{ matrix.ruby-version }}
33
30
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
data/Gemfile.lock CHANGED
@@ -1,40 +1,39 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- schema-test (0.1.0)
4
+ schema-test (0.2.0)
5
5
  json
6
6
  json_schemer
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
+ bigdecimal (3.1.8)
11
12
  byebug (11.1.3)
12
- diff-lcs (1.4.4)
13
- ecma-re-validator (0.3.0)
14
- regexp_parser (~> 2.0)
13
+ diff-lcs (1.5.1)
15
14
  hana (1.3.7)
16
- json (2.5.1)
17
- json_schemer (0.2.18)
18
- ecma-re-validator (~> 0.3)
15
+ json (2.7.2)
16
+ json_schemer (2.3.0)
17
+ bigdecimal
19
18
  hana (~> 1.3)
20
19
  regexp_parser (~> 2.0)
21
- uri_template (~> 0.7)
22
- rake (10.5.0)
23
- regexp_parser (2.1.1)
24
- rspec (3.10.0)
25
- rspec-core (~> 3.10.0)
26
- rspec-expectations (~> 3.10.0)
27
- rspec-mocks (~> 3.10.0)
28
- rspec-core (3.10.1)
29
- rspec-support (~> 3.10.0)
30
- rspec-expectations (3.10.1)
20
+ simpleidn (~> 0.2)
21
+ rake (13.2.1)
22
+ regexp_parser (2.9.2)
23
+ rspec (3.13.0)
24
+ rspec-core (~> 3.13.0)
25
+ rspec-expectations (~> 3.13.0)
26
+ rspec-mocks (~> 3.13.0)
27
+ rspec-core (3.13.0)
28
+ rspec-support (~> 3.13.0)
29
+ rspec-expectations (3.13.0)
31
30
  diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.10.0)
33
- rspec-mocks (3.10.2)
31
+ rspec-support (~> 3.13.0)
32
+ rspec-mocks (3.13.0)
34
33
  diff-lcs (>= 1.2.0, < 2.0)
35
- rspec-support (~> 3.10.0)
36
- rspec-support (3.10.2)
37
- uri_template (0.7.0)
34
+ rspec-support (~> 3.13.0)
35
+ rspec-support (3.13.1)
36
+ simpleidn (0.2.3)
38
37
 
39
38
  PLATFORMS
40
39
  ruby
@@ -42,9 +41,9 @@ PLATFORMS
42
41
  DEPENDENCIES
43
42
  bundler (~> 2)
44
43
  byebug
45
- rake (~> 10.0)
44
+ rake (~> 13.0)
46
45
  rspec (~> 3.0)
47
46
  schema-test!
48
47
 
49
48
  BUNDLED WITH
50
- 2.1.4
49
+ 2.5.9
data/README.md CHANGED
@@ -129,6 +129,7 @@ require 'schema_test/minitest'
129
129
  SchemaTest.configure do |config|
130
130
  config.domain = 'mydomain.com'
131
131
  config.definition_paths << Rails.root.join('test', 'schema_definitions')
132
+ config.disable_rubocop = true # optional, set to true if using rubocop to disable it in the generated code
132
133
  end
133
134
  SchemaTest.load!
134
135
  ```
@@ -190,6 +191,64 @@ end
190
191
  ```
191
192
  Keeping the full schema directly in the tests means that it is **impossible** for us to accidentally impact any API endpoints with a distant schema change without also producing some change in the test files for those endpoints. That is the main benefit that this library tries to acheive.
192
193
 
194
+ ### Compiled mode
195
+
196
+ As an alternative to inlining expanded schemas into your test files, you can use **compiled mode**. In this mode, all schema definitions are compiled to standalone JSON Schema files on disk, and test assertions validate against those files directly. No test file rewriting happens.
197
+
198
+ To enable it, update your `test_helper.rb`:
199
+
200
+ ``` ruby
201
+ require 'schema_test/minitest'
202
+ SchemaTest.configure do |config|
203
+ config.domain = 'mydomain.com'
204
+ config.definition_paths << Rails.root.join('test', 'schema_definitions')
205
+ config.compiled = true
206
+ end
207
+ SchemaTest.compile!
208
+ ```
209
+
210
+ This will create a `compiled` directory inside your schema definitions directory containing one JSON file per definition. The compiled files mirror the layout of your original definition files, so a definition declared in `api/v3/film.rb` is compiled to `compiled/api/v3/film.json`:
211
+
212
+ ```
213
+ test/schema_definitions/
214
+ ├── api/
215
+ │ └── v3/
216
+ │ ├── film.rb
217
+ │ └── user.rb
218
+ └── compiled/
219
+ └── api/
220
+ └── v3/
221
+ ├── film.json
222
+ ├── user.v1.json
223
+ └── user.v2.json
224
+ ```
225
+
226
+ Versioned definitions are named `<name>.v<version>.json`; unversioned ones are simply `<name>.json`. Definitions without a known source location are written to the root of the `compiled` directory.
227
+
228
+ Your test assertions stay the same — `assert_valid_json_for_schema` will load the pre-compiled JSON schema file and validate against it:
229
+
230
+ ``` ruby
231
+ test 'JSON returned matches schema' do
232
+ json = JSON.parse(response.body)
233
+ assert_valid_json_for_schema(json, :user, version: 1)
234
+ end
235
+ ```
236
+
237
+ You can commit the compiled JSON files to your repository so that the schemas used in tests are visible in diffs, or add `compiled/` to your `.gitignore` and regenerate them as part of your test setup.
238
+
239
+ #### Schema fingerprints
240
+
241
+ In compiled mode the test files no longer contain the expanded schema, so a schema change wouldn't otherwise show up as a diff in the tests themselves. To keep that feedback, each assertion records a `fingerprint:` argument — a SHA-256 of the compiled schema:
242
+
243
+ ``` ruby
244
+ test 'JSON returned matches schema' do
245
+ json = JSON.parse(response.body)
246
+ assert_valid_json_for_schema(json, :user, version: 1, fingerprint: '9f2c…')
247
+ end
248
+ ```
249
+
250
+ You don't write the fingerprint by hand. When you run the tests locally, the assertion compares the fingerprint argument against the compiled schema and, if it is missing or stale, rewrites it in place. The resulting diff points at exactly which API endpoints changed. In CI (when `ENV['CI']` is set) a mismatched or missing fingerprint fails the test instead of rewriting it, so out-of-date assertions can't be merged.
251
+
193
252
  ## Development
194
253
 
195
254
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,58 @@
1
+ module SchemaTest
2
+ class Collapser
3
+ def initialize(contents)
4
+ @lines = contents.split("\n")
5
+ end
6
+
7
+ def output
8
+ result = []
9
+ i = 0
10
+ while i < @lines.length
11
+ line = @lines[i]
12
+
13
+ # Skip rubocop:disable line immediately before an EXPANDED block
14
+ if line.match?(/#{DISABLE_RUBOCOP_COMMENT}/) &&
15
+ i + 1 < @lines.length && @lines[i + 1].match?(/#{OPENING_COMMENT}/)
16
+ i += 1
17
+ next
18
+ end
19
+
20
+ if line =~ /\A(\s*)(\w+)\(\s*#{OPENING_COMMENT}/
21
+ indent = $1
22
+ method_name = $2
23
+
24
+ # Next line is the json variable
25
+ json_var = @lines[i + 1].strip.sub(/,\z/, '')
26
+
27
+ # Gather remaining content lines until closing marker
28
+ content_lines = []
29
+ j = i + 2
30
+ while j < @lines.length && !@lines[j].match?(/#{CLOSING_COMMENT}/)
31
+ content_lines << @lines[j]
32
+ j += 1
33
+ end
34
+ end_index = j
35
+
36
+ # Extract name and version from the content
37
+ joined = content_lines.map(&:strip).join(' ')
38
+ name = joined.match(/:(\w+),/)[1]
39
+ version_match = joined.match(/:version\s*=>\s*([^,}]+)/)
40
+ version = version_match[1].strip
41
+
42
+ result << "#{indent}#{method_name}(#{json_var}, :#{name}, version: #{version})"
43
+
44
+ # Skip rubocop:enable line immediately after END EXPANDED
45
+ i = end_index + 1
46
+ if i < @lines.length && @lines[i].match?(/#{ENABLE_RUBOCOP_COMMENT}/)
47
+ i += 1
48
+ end
49
+ else
50
+ result << line
51
+ i += 1
52
+ end
53
+ end
54
+
55
+ result.join("\n") + "\n"
56
+ end
57
+ end
58
+ end
@@ -2,9 +2,12 @@ require 'schema_test/property'
2
2
 
3
3
  module SchemaTest
4
4
  class Collection < SchemaTest::Property::Object
5
- def initialize(name, of_name, version: nil, description: nil)
5
+ attr_reader :location
6
+
7
+ def initialize(name, of_name, location: nil, version: nil, description: nil)
6
8
  super(name, version: version, description: description)
7
9
  @item_type = lookup_object(of_name, version)
10
+ @location = location
8
11
  SchemaTest::Definition.register(self)
9
12
  end
10
13
 
@@ -9,9 +9,19 @@ module SchemaTest
9
9
  # files in as many subdirectories are you like.
10
10
  attr_accessor :definition_paths
11
11
 
12
+ # Set to true in order to disable running rubocop on the expanded schemas,
13
+ # as the Ruby PP output does not conform to rubocop's standards.
14
+ attr_accessor :disable_rubocop
15
+
16
+ # Set to true to use pre-compiled JSON schema files for validation
17
+ # instead of inlining expanded schemas into test files.
18
+ attr_accessor :compiled
19
+
12
20
  def initialize
13
21
  @domain = 'example.com'
14
22
  @definition_paths = []
23
+ @disable_rubocop = false
24
+ @compiled = false
15
25
  end
16
26
  end
17
27
  end
@@ -16,28 +16,22 @@ module SchemaTest
16
16
  (@definitions || {}).dig(name, version)
17
17
  end
18
18
 
19
+ def self.all
20
+ (@definitions || {}).values.flat_map(&:values)
21
+ end
22
+
19
23
  def self.find!(name, version)
20
24
  found = find(name, version)
21
- raise "Could not find schema for #{name.inspect} (version: #{version.inspect})" unless found
25
+ raise SchemaTest::Error, "Could not find schema for #{name.inspect} (version: #{version.inspect})" unless found
22
26
  found
23
27
  end
24
28
 
25
- def initialize(*args)
26
- super
27
- self.class.register(self)
28
- end
29
-
30
- def type(name, version=nil)
31
- lookup_object(name, version || @version)
32
- end
29
+ attr_reader :location
33
30
 
34
- def optional(object)
35
- object.optional!
36
- end
37
-
38
- def as_structure(_=nil)
39
- hashes, others = @properties.values.map(&:as_structure).partition { |x| x.is_a?(Hash) }
40
- others + [hashes.inject(&:merge)].compact
31
+ def initialize(name, location: nil, **attributes)
32
+ super(name, **attributes)
33
+ self.class.register(self)
34
+ @location = location
41
35
  end
42
36
 
43
37
  def as_json_schema(domain: SchemaTest.configuration.domain)
@@ -48,12 +42,5 @@ module SchemaTest
48
42
  'title' => name.to_s
49
43
  }.merge(super(false))
50
44
  end
51
-
52
- def based_on(name, version: self.version)
53
- other_version = self.class.find(name, version)
54
- other_version.properties.values.each do |property|
55
- define_property(property.dup)
56
- end
57
- end
58
45
  end
59
46
  end
@@ -0,0 +1,93 @@
1
+ module SchemaTest
2
+ # Rewrites `assert_valid_json_for_schema` calls in test files so that
3
+ # they carry an up-to-date `fingerprint:` argument matching the
4
+ # compiled schema. This means a schema change shows up as a diff in
5
+ # the test files themselves, pointing at exactly which API endpoints
6
+ # changed, and lets the assertion verify the fingerprint at runtime.
7
+ #
8
+ # The call site reported at runtime is the line the call *starts* on,
9
+ # which for a multi-line call is the line with the opening paren. The
10
+ # rewriter therefore scans forward to find the matching closing paren
11
+ # before inserting or replacing the argument.
12
+ class FingerprintRewriter
13
+ FINGERPRINT_ARGUMENT = /fingerprint:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s,)]+)/
14
+
15
+ def initialize(contents, line_indexes_with_fingerprints)
16
+ @lines = contents.split("\n")
17
+ @line_indexes_with_fingerprints = line_indexes_with_fingerprints
18
+ end
19
+
20
+ def output
21
+ @line_indexes_with_fingerprints.each do |start_index, fingerprint|
22
+ next unless @lines[start_index]
23
+ annotate_call(start_index, fingerprint)
24
+ end
25
+ @lines.join("\n") + "\n"
26
+ end
27
+
28
+ private
29
+
30
+ def annotate_call(start_index, fingerprint)
31
+ close = find_closing_paren(start_index)
32
+ return unless close
33
+ end_index, paren_column = close
34
+
35
+ # If the call already carries a fingerprint argument, replace it.
36
+ (start_index..end_index).each do |index|
37
+ if @lines[index] =~ FINGERPRINT_ARGUMENT
38
+ @lines[index] = @lines[index].sub(FINGERPRINT_ARGUMENT, "fingerprint: '#{fingerprint}'")
39
+ return
40
+ end
41
+ end
42
+
43
+ argument = "fingerprint: '#{fingerprint}'"
44
+ close_line = @lines[end_index]
45
+ before_paren = close_line[0...paren_column]
46
+
47
+ if before_paren.strip.empty?
48
+ # The closing paren is on its own line; append the argument to the
49
+ # last argument line so we don't leave a leading comma dangling.
50
+ insert_index = (start_index...end_index).to_a.reverse.find { |index| !@lines[index].strip.empty? } || start_index
51
+ @lines[insert_index] = "#{@lines[insert_index].sub(/,\s*\z/, '')}, #{argument}"
52
+ else
53
+ @lines[end_index] = "#{before_paren.sub(/,\s*\z/, '')}, #{argument}#{close_line[paren_column..-1]}"
54
+ end
55
+ end
56
+
57
+ # Finds the closing paren matching the first opening paren at or after
58
+ # the start line, returning [line_index, column] or nil. Skips parens
59
+ # inside string literals and trailing comments.
60
+ def find_closing_paren(start_index)
61
+ depth = 0
62
+ started = false
63
+ (start_index...@lines.length).each do |line_index|
64
+ line = @lines[line_index]
65
+ string_delimiter = nil
66
+ column = 0
67
+ while column < line.length
68
+ char = line[column]
69
+ if string_delimiter
70
+ if char == '\\'
71
+ column += 2
72
+ next
73
+ elsif char == string_delimiter
74
+ string_delimiter = nil
75
+ end
76
+ elsif char == '"' || char == "'"
77
+ string_delimiter = char
78
+ elsif char == '#'
79
+ break
80
+ elsif char == '('
81
+ depth += 1
82
+ started = true
83
+ elsif char == ')'
84
+ depth -= 1
85
+ return [line_index, column] if started && depth.zero?
86
+ end
87
+ column += 1
88
+ end
89
+ end
90
+ nil
91
+ end
92
+ end
93
+ end
@@ -2,23 +2,41 @@ require 'schema_test'
2
2
 
3
3
  module SchemaTest
4
4
  module Minitest
5
- def assert_valid_json_for_schema(json, name, version:, schema: nil)
6
- install_assert_api_expansion_hook
5
+ def assert_valid_json_for_schema(json, name, arguments)
6
+ version = arguments[:version]
7
7
 
8
- definition = SchemaTest::Definition.find(name, version)
9
- raise "Unknown definition #{name}, version: #{version}" unless definition.present?
8
+ if SchemaTest.configuration.compiled
9
+ schema = SchemaTest.load_compiled_schema(name, version: version)
10
+ actual_fingerprint = SchemaTest.schema_fingerprint(schema)
10
11
 
11
- expected_schema = definition.as_json_schema
12
+ if arguments[:fingerprint] != actual_fingerprint
13
+ if ENV['CI']
14
+ flunk "Schema fingerprint mismatch for #{name.inspect} (version: #{version.inspect}) at #{caller[0]}. The compiled schema has changed; run the tests locally to update the fingerprint."
15
+ else
16
+ install_fingerprint_rewrite_hook
17
+ queue_write_schema_fingerprint(caller[0], actual_fingerprint)
18
+ end
19
+ end
20
+
21
+ assert_json_schema_validates_against(json, schema)
22
+ else
23
+ install_assert_api_expansion_hook
24
+
25
+ schema = arguments[:schema]
26
+
27
+ definition = SchemaTest::Definition.find(name, version)
28
+ raise "Unknown definition #{name}, version: #{version}" unless definition.present?
29
+
30
+ expected_schema = definition.as_json_schema
12
31
 
13
- if schema != expected_schema
14
- if ENV['CI']
32
+ if schema != expected_schema && ENV['CI']
15
33
  flunk "Outdated API schema assertion at #{caller[0]}"
16
- else
17
- queue_write_expanded_assert_api_call(caller[0], __method__, name, version, expected_schema)
18
34
  end
19
- end
20
35
 
21
- assert_json_schema_validates_against(json, expected_schema)
36
+ queue_write_expanded_assert_api_call(caller[0], __method__, name, version, definition.location, expected_schema)
37
+
38
+ assert_json_schema_validates_against(json, expected_schema)
39
+ end
22
40
  end
23
41
 
24
42
  def assert_json_schema_validates_against(json, schema)
@@ -31,12 +49,17 @@ module SchemaTest
31
49
  @@__api_schema_calls_for_expansion = {}
32
50
  @@__api_schema_expansion_hook_installed = false
33
51
 
34
- def queue_write_expanded_assert_api_call(call_site, method, name, version, expected_schema)
52
+ def queue_write_expanded_assert_api_call(call_site, method, name, version, location, expected_schema)
35
53
  file, line = call_site.split(':')
36
54
  line_index = line.to_i.pred
55
+ schema_call = [line_index, method, name, version, location, expected_schema]
37
56
 
38
57
  @@__api_schema_calls_for_expansion[file] ||= []
39
- @@__api_schema_calls_for_expansion[file] << [line_index, method, name, version, expected_schema]
58
+ if (existing_call = @@__api_schema_calls_for_expansion[file].find { |call| line_index == call[0] })
59
+ return if existing_call == schema_call
60
+ raise "Expected schema does not match for duplicate API schema assertion at #{call_site}"
61
+ end
62
+ @@__api_schema_calls_for_expansion[file] << [line_index, method, name, version, location, expected_schema]
40
63
  end
41
64
 
42
65
  def install_assert_api_expansion_hook
@@ -48,11 +71,36 @@ module SchemaTest
48
71
  def expand_assert_api_calls
49
72
  @@__api_schema_calls_for_expansion.each do |file, line_indexes_with_schemas|
50
73
  original_contents = File.read(file)
51
- rewriter = SchemaTest::Rewriter.new(original_contents, line_indexes_with_schemas)
74
+ rewriter_options = { disable_rubocop: SchemaTest.configuration.disable_rubocop }
75
+ rewriter = SchemaTest::Rewriter.new(original_contents, line_indexes_with_schemas, options: rewriter_options)
52
76
  new_contents = rewriter.output
53
77
  raise "Error rewriting file" if new_contents.blank?
54
78
  File.open(file, 'w') { |f| f.puts new_contents }
55
79
  end
56
80
  end
81
+
82
+ @@__schema_fingerprints = {}
83
+ @@__schema_fingerprint_hook_installed = false
84
+
85
+ def queue_write_schema_fingerprint(call_site, fingerprint)
86
+ file, line = call_site.split(':')
87
+ line_index = line.to_i.pred
88
+ @@__schema_fingerprints[file] ||= {}
89
+ @@__schema_fingerprints[file][line_index] = fingerprint
90
+ end
91
+
92
+ def install_fingerprint_rewrite_hook
93
+ return if @@__schema_fingerprint_hook_installed
94
+ at_exit { write_schema_fingerprints }
95
+ @@__schema_fingerprint_hook_installed = true
96
+ end
97
+
98
+ def write_schema_fingerprints
99
+ @@__schema_fingerprints.each do |file, line_indexes_with_fingerprints|
100
+ original_contents = File.read(file)
101
+ rewriter = SchemaTest::FingerprintRewriter.new(original_contents, line_indexes_with_fingerprints)
102
+ File.open(file, 'w') { |f| f.print rewriter.output }
103
+ end
104
+ end
57
105
  end
58
106
  end
@@ -1,20 +1,15 @@
1
1
  module SchemaTest
2
2
  class Property
3
- attr_reader :name, :type, :description, :optional
3
+ NULL_TYPE = 'null'.freeze
4
+
5
+ attr_reader :name, :_type, :description
4
6
 
5
7
  def initialize(name, type, description=nil)
6
8
  @name = name
7
- @type = type
9
+ @_type = type
8
10
  @description = description
9
11
  @optional = false
10
- end
11
-
12
- def as_structure(_=nil)
13
- if @optional
14
- { name => nil }
15
- else
16
- name
17
- end
12
+ @nullable = false
18
13
  end
19
14
 
20
15
  def as_json_schema
@@ -25,28 +20,69 @@ module SchemaTest
25
20
  end
26
21
 
27
22
  def ==(other)
23
+ return false unless other.is_a?(SchemaTest::Property)
24
+
28
25
  name == other.name &&
29
- type == other.type &&
26
+ _type == other._type &&
30
27
  description == other.description &&
31
- optional == other.optional
28
+ optional? == other.optional? &&
29
+ nullable? == other.nullable?
30
+ end
31
+
32
+ def optional(object)
33
+ object.tap(&:optional!)
34
+ end
35
+
36
+ def nullable(object)
37
+ object.tap(&:nullable!)
32
38
  end
33
39
 
34
40
  def optional?
35
- optional
41
+ @optional
36
42
  end
37
43
 
38
44
  def optional!
39
45
  @optional = true
40
46
  end
41
47
 
48
+ def nullable?
49
+ @nullable
50
+ end
51
+
52
+ def nullable!
53
+ @nullable = true
54
+ end
55
+
56
+ def lookup_object(name, *versions)
57
+ UnresolvedProperty.new(name, versions: versions)
58
+ end
59
+
60
+ def type(name, version: nil)
61
+ lookup_object(name, version || @version)
62
+ end
63
+
64
+ def base_json_schema_type
65
+ @_type.to_s
66
+ end
67
+
42
68
  def json_schema_type
43
- @type.to_s
69
+ if nullable?
70
+ [base_json_schema_type, NULL_TYPE]
71
+ else
72
+ base_json_schema_type
73
+ end
44
74
  end
45
75
 
46
76
  def json_schema_format
47
77
  nil
48
78
  end
49
79
 
80
+ class Nil < SchemaTest::Property
81
+ def initialize(name, description=nil)
82
+ super(name, :null, description)
83
+ end
84
+ end
85
+
50
86
  class Boolean < SchemaTest::Property
51
87
  def initialize(name, description=nil)
52
88
  super(name, :boolean, description)
@@ -64,7 +100,7 @@ module SchemaTest
64
100
  super(name, :float, description)
65
101
  end
66
102
 
67
- def json_schema_type
103
+ def base_json_schema_type
68
104
  'number'
69
105
  end
70
106
  end
@@ -75,12 +111,26 @@ module SchemaTest
75
111
  end
76
112
  end
77
113
 
114
+ class Date < SchemaTest::Property
115
+ def initialize(name, description=nil)
116
+ super(name, :date, description)
117
+ end
118
+
119
+ def base_json_schema_type
120
+ 'string'
121
+ end
122
+
123
+ def json_schema_format
124
+ 'date'
125
+ end
126
+ end
127
+
78
128
  class DateTime < SchemaTest::Property
79
129
  def initialize(name, description=nil)
80
130
  super(name, :datetime, description)
81
131
  end
82
132
 
83
- def json_schema_type
133
+ def base_json_schema_type
84
134
  'string'
85
135
  end
86
136
 
@@ -96,30 +146,37 @@ module SchemaTest
96
146
  end
97
147
 
98
148
  class SchemaTest::Property::Object < SchemaTest::Property
99
- attr_reader :version
149
+ attr_reader :version, :excluded_property_names
100
150
 
101
- def initialize(name, description: nil, version: nil, from: nil, properties: nil, &block)
102
- @name = name
103
- @description = description
151
+ def initialize(name, description: nil, version: nil, from: nil, properties: nil, except: [], &block)
152
+ super(name, :object, description)
104
153
  @version = version
105
154
  @specific_properties = properties
106
155
  @properties = {}
156
+ @excluded_property_names = except
107
157
  @from = from
108
158
  instance_eval(&block) if block_given?
109
159
  end
110
160
 
111
161
  def properties
112
162
  resolve
113
- @properties
163
+ @properties.reject { |p| excluded_property_names.include?(p) }
164
+ end
165
+
166
+ def based_on(name, version: self.version, except: [])
167
+ @from = lookup_object(name, version)
168
+ @excluded_property_names = except
114
169
  end
115
170
 
116
171
  def ==(other)
117
- super && properties.all? { |name, property| property == other.properties[name] }
172
+ super &&
173
+ properties.all? { |name, property| property == other.properties[name] } &&
174
+ excluded_property_names == other.excluded_property_names
118
175
  end
119
176
 
120
177
  def resolve
121
178
  if @from
122
- @properties.merge!(@from.properties)
179
+ @properties = @from.properties.merge(@properties)
123
180
  @from = nil
124
181
  end
125
182
  if @specific_properties
@@ -146,8 +203,10 @@ module SchemaTest
146
203
  float: SchemaTest::Property::Float,
147
204
  string: SchemaTest::Property::String,
148
205
  datetime: SchemaTest::Property::DateTime,
206
+ date: SchemaTest::Property::Date,
149
207
  url: SchemaTest::Property::Uri,
150
- html: SchemaTest::Property::String
208
+ html: SchemaTest::Property::String,
209
+ null: SchemaTest::Property::Nil
151
210
  }
152
211
 
153
212
  TYPES.each do |method_name, type_class|
@@ -160,12 +219,20 @@ module SchemaTest
160
219
  define_property(SchemaTest::Property::Array.new(name, of, desc, &block))
161
220
  end
162
221
 
163
- def object(name, desc: nil, as: name, version: nil, &block)
222
+ def object(name, desc: nil, as: name, version: nil, except: [], &block)
164
223
  inferred_version = version || @version
165
224
  if block_given?
166
225
  define_property(SchemaTest::Property::Object.new(as, description: desc, version: inferred_version, &block))
167
226
  else
168
- define_property(SchemaTest::Property::Object.new(as, description: desc, version: inferred_version, from: lookup_object(name, inferred_version)))
227
+ define_property(
228
+ SchemaTest::Property::Object.new(
229
+ as,
230
+ description: desc,
231
+ version: inferred_version,
232
+ from: lookup_object(name, inferred_version, nil),
233
+ except: except
234
+ )
235
+ )
169
236
  end
170
237
  end
171
238
 
@@ -185,15 +252,7 @@ module SchemaTest
185
252
  end
186
253
  end
187
254
 
188
- def as_structure(include_root=true)
189
- if include_root
190
- { name => properties.values.map(&:as_structure) }
191
- else
192
- properties.values.map(&:as_structure)
193
- end
194
- end
195
-
196
- def json_schema_type
255
+ def base_json_schema_type
197
256
  'object'
198
257
  end
199
258
 
@@ -202,15 +261,20 @@ module SchemaTest
202
261
  def define_property(attribute)
203
262
  @properties[attribute.name] = attribute
204
263
  end
205
-
206
- def lookup_object(name, version)
207
- UnresolvedProperty.new(name, version: version)
208
- end
209
264
  end
210
265
 
211
266
  class UnresolvedProperty < SchemaTest::Property::Object
267
+ def initialize(name, versions:)
268
+ @name = name
269
+ @versions = versions
270
+ end
271
+
212
272
  def resolve
213
- SchemaTest::Definition.find!(name, version)
273
+ @versions.each do |v|
274
+ definition = SchemaTest::Definition.find(@name, v)
275
+ return definition if definition
276
+ end
277
+ raise SchemaTest::Error, "could not resolve schema #{@name.inspect}; tried versions: #{@versions.inspect}"
214
278
  end
215
279
 
216
280
  def ==(other)
@@ -220,20 +284,12 @@ module SchemaTest
220
284
  def properties
221
285
  resolve.properties
222
286
  end
223
-
224
- def as_structure(*args)
225
- resolve.as_structure(*args)
226
- end
227
287
  end
228
288
 
229
289
  class AnonymousObject < SchemaTest::Property::Object
230
290
  def initialize(properties: nil, &block)
231
291
  super(nil, properties: properties, &block)
232
292
  end
233
-
234
- def as_structure(_=nil)
235
- super(false)
236
- end
237
293
  end
238
294
 
239
295
  class Array < SchemaTest::Property
@@ -253,14 +309,6 @@ module SchemaTest
253
309
  super && @item_type == other.item_type
254
310
  end
255
311
 
256
- def as_structure(_=nil)
257
- if @item_type.is_a?(SchemaTest::Property)
258
- { name => @item_type.as_structure(false) }
259
- else
260
- { name => [] }
261
- end
262
- end
263
-
264
312
  def as_json_schema
265
313
  super.tap do |json_schema|
266
314
  item_schema = @item_type.is_a?(SchemaTest::Property) ? @item_type.as_json_schema(false) : { 'type' => @item_type.to_s }
@@ -4,23 +4,37 @@ module SchemaTest
4
4
  OPENING_COMMENT = '# EXPANDED'.freeze
5
5
  CLOSING_COMMENT = '# END EXPANDED'.freeze
6
6
 
7
+ DISABLE_RUBOCOP_COMMENT = '# rubocop:disable all'.freeze
8
+ ENABLE_RUBOCOP_COMMENT = '# rubocop:enable all'.freeze
9
+
7
10
  class Rewriter
8
- def initialize(contents, line_indexes_with_schemas)
11
+ def initialize(contents, line_indexes_with_schemas, options: {})
9
12
  @lines = contents.split("\n")
10
13
  @line_indexes_with_schemas = line_indexes_with_schemas
14
+
15
+ @disable_rubocop = options.fetch(:disable_rubocop, false)
11
16
  end
12
17
 
13
18
  def output
14
19
  current_offset = 0
15
- line_indexes_with_schemas.sort_by { |(line,_)| line }.each do |index, method, name, version, expected_schema|
20
+ line_indexes_with_schemas.sort_by { |(line,_)| line }.each do |index, method, name, version, location, expected_schema|
16
21
  start_index = index + current_offset
17
- if lines[start_index] =~ /#{OPENING_COMMENT}\s*\z/
22
+ if lines[start_index - 1].match?(/#{DISABLE_RUBOCOP_COMMENT}/)
23
+ lines.delete_at(start_index - 1)
24
+ start_index -= 1
25
+ current_offset -= 2
26
+ end
27
+
28
+ if lines[start_index] =~ /#{OPENING_COMMENT}/
18
29
  end_index = start_index + lines[start_index..-1].find_index { |line| line =~ /#{CLOSING_COMMENT}\s*\z/ }
30
+ lines.delete_at(end_index + 1) if lines[end_index + 1]&.match?(/#{ENABLE_RUBOCOP_COMMENT}/)
19
31
  json_variable_name = lines[start_index + 1].strip.gsub(/,\z/, '')
20
32
  else
21
33
  end_index = start_index
22
34
  json_variable_name = lines[start_index].match(/\(([^,]+)/)[1]
23
35
  end
36
+
37
+ original_method_definition_length = end_index - start_index
24
38
  start_indent = lines[start_index].match(/\A(\s*)/)[0].length
25
39
  (end_index - start_index + 1).times { |i| lines.delete_at(start_index) }
26
40
 
@@ -31,14 +45,16 @@ module SchemaTest
31
45
  expanded_schema_lines.unshift(json_variable_name + ',')
32
46
 
33
47
  method_string = [
34
- (' ' * start_indent) + method.to_s + "( #{OPENING_COMMENT}",
48
+ disable_rubocop ? (' ' * start_indent) + DISABLE_RUBOCOP_COMMENT : nil,
49
+ (' ' * start_indent) + method.to_s + "( #{OPENING_COMMENT} from #{location}",
35
50
  *expanded_schema_lines.map { |line| (' ' * (start_indent + 2)) + line },
36
- (' ' * start_indent) + ") #{CLOSING_COMMENT}"
37
- ]
51
+ (' ' * start_indent) + ") #{CLOSING_COMMENT}",
52
+ disable_rubocop ? (' ' * start_indent) + ENABLE_RUBOCOP_COMMENT: nil,
53
+ ].compact
38
54
 
39
55
  method_string.reverse.each { |line| lines.insert(start_index, line) }
40
56
 
41
- current_offset += method_string.count - 1
57
+ current_offset += method_string.count - original_method_definition_length - 1
42
58
  end
43
59
 
44
60
  lines.compact.join("\n") + "\n"
@@ -47,5 +63,6 @@ module SchemaTest
47
63
  private
48
64
 
49
65
  attr_reader :lines, :line_indexes_with_schemas
66
+ attr_reader :disable_rubocop
50
67
  end
51
68
  end
@@ -40,9 +40,13 @@ module SchemaTest
40
40
  when 'format'
41
41
  "format should be #{error['schema']['format']}"
42
42
  when 'required'
43
- "missing some required attributes"
43
+ "missing some required attributes: #{error['details'].inspect}"
44
44
  else
45
- "type should be #{error['type']}"
45
+ if error['type'] == 'type'
46
+ "type should be one of #{error['schema']['type'].inspect}"
47
+ else
48
+ "type should be #{error['type']}"
49
+ end
46
50
  end
47
51
  "value at #{error['data_pointer']} (#{error['data'].inspect}) failed validation: #{message}"
48
52
  end
@@ -1,3 +1,3 @@
1
1
  module SchemaTest
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/schema_test.rb CHANGED
@@ -1,5 +1,9 @@
1
+ require 'json'
2
+ require 'digest'
1
3
  require 'schema_test/version'
2
4
  require 'schema_test/rewriter'
5
+ require 'schema_test/fingerprint_rewriter'
6
+ require 'schema_test/collapser'
3
7
  require 'schema_test/definition'
4
8
  require 'schema_test/collection'
5
9
  require 'schema_test/validator'
@@ -10,6 +14,13 @@ module SchemaTest
10
14
 
11
15
  SCHEMA_VERSION = "http://json-schema.org/draft-07/schema#"
12
16
 
17
+ # The number of leading hex characters of the schema digest kept as a
18
+ # fingerprint. The fingerprint only needs to change when the schema
19
+ # changes (it is compared against a single expected value, never
20
+ # searched for collisions), so a short prefix keeps the assertion lines
21
+ # readable while remaining collision-free in practice.
22
+ FINGERPRINT_LENGTH = 12
23
+
13
24
  class << self
14
25
  def reset!
15
26
  @configuration = nil
@@ -33,7 +44,7 @@ module SchemaTest
33
44
 
34
45
  # Define a new schema
35
46
  def define(name, collection: nil, **attributes, &block)
36
- definition = SchemaTest::Definition.new(name, attributes, &block)
47
+ definition = SchemaTest::Definition.new(name, location: definition_location(caller[0]), **attributes, &block)
37
48
  if collection
38
49
  collection(collection, of: name, version: attributes[:version])
39
50
  end
@@ -43,7 +54,67 @@ module SchemaTest
43
54
  # Explicitly define a new schema collection (an array of other schema
44
55
  # objects)
45
56
  def collection(name, of:, **attributes)
46
- SchemaTest::Collection.new(name, of, attributes)
57
+ SchemaTest::Collection.new(name, of, location: definition_location(caller[1]), **attributes)
58
+ end
59
+
60
+ # Compile all definitions to JSON Schema files in a `compiled`
61
+ # directory within each definition path. The compiled files mirror
62
+ # the layout of the original definition files, so a definition
63
+ # declared in `api/v3/film.rb` is written to
64
+ # `compiled/api/v3/film.json`.
65
+ def compile!
66
+ load_definitions
67
+ SchemaTest::Definition.all.each do |definition|
68
+ begin
69
+ definition_path = owning_definition_path(definition)
70
+ next unless definition_path
71
+ path = Pathname.new(definition_path).join('compiled', compiled_relative_path(definition))
72
+ path.dirname.mkpath
73
+ File.write(path, JSON.pretty_generate(definition.as_json_schema) + "\n")
74
+ rescue => e
75
+ warn "SchemaTest: failed to compile #{definition.name} (version: #{definition.version}): #{e.message}"
76
+ end
77
+ end
78
+ end
79
+
80
+ # Load a pre-compiled JSON schema from the compiled directory.
81
+ # Because compiled files mirror the original definition layout, the
82
+ # file may live in a nested subdirectory, so the compiled tree is
83
+ # searched recursively for the expected filename.
84
+ def load_compiled_schema(name, version: nil)
85
+ filename = compiled_filename(name, version)
86
+ configuration.definition_paths.each do |definition_path|
87
+ compiled_root = Pathname.new(definition_path).join('compiled')
88
+ match = Pathname.glob(compiled_root.join('**', filename)).first
89
+ return JSON.parse(match.read) if match
90
+ end
91
+ raise SchemaTest::Error, "Could not find compiled schema for #{name.inspect} (version: #{version.inspect})"
92
+ end
93
+
94
+ # A stable fingerprint of a compiled schema. The fingerprint is
95
+ # derived from the schema's semantic content, so it changes whenever
96
+ # the schema changes but is unaffected by pretty-print formatting.
97
+ def schema_fingerprint(schema)
98
+ Digest::SHA256.hexdigest(JSON.generate(schema))[0, FINGERPRINT_LENGTH]
99
+ end
100
+
101
+ # Collapse expanded schema assertions in test files back to
102
+ # simple one-line calls. Pass file paths or directory paths.
103
+ # Directories are globbed for **/*.rb files.
104
+ def collapse!(*paths)
105
+ files = paths.flat_map do |path|
106
+ if File.directory?(path)
107
+ Dir[File.join(path, '**', '*.rb')]
108
+ else
109
+ [path]
110
+ end
111
+ end
112
+ files.each do |file|
113
+ contents = File.read(file)
114
+ next unless contents.include?(OPENING_COMMENT)
115
+ collapser = SchemaTest::Collapser.new(contents)
116
+ File.write(file, collapser.output)
117
+ end
47
118
  end
48
119
 
49
120
  # Validate some JSON data against a schema or schema definition
@@ -58,9 +129,69 @@ module SchemaTest
58
129
 
59
130
  private
60
131
 
132
+ # The filename a definition compiles to, e.g. `film.json` or
133
+ # `film.v2.json` for versioned definitions.
134
+ def compiled_filename(name, version)
135
+ if version
136
+ "#{name}.v#{version}.json"
137
+ else
138
+ "#{name}.json"
139
+ end
140
+ end
141
+
142
+ # The relative source file a definition was declared in (without the
143
+ # trailing line number), or nil if it has no known location.
144
+ def definition_source_file(definition)
145
+ return nil unless definition.location
146
+ source = definition.location.rpartition(':').first
147
+ source = definition.location if source.empty?
148
+ source.empty? ? nil : source
149
+ end
150
+
151
+ # The definition path a definition's source file lives under, so
152
+ # that each schema is compiled exactly once into the `compiled`
153
+ # directory of its owning path rather than duplicated into every
154
+ # definition path. Definitions whose source cannot be located (for
155
+ # example, those constructed directly without a location) fall back
156
+ # to the first configured definition path.
157
+ def owning_definition_path(definition)
158
+ source = definition_source_file(definition)
159
+ if source
160
+ owning = configuration.definition_paths.find do |definition_path|
161
+ Pathname.new(definition_path).join(source).exist?
162
+ end
163
+ return owning if owning
164
+ end
165
+ configuration.definition_paths.first
166
+ end
167
+
168
+ # The path a definition compiles to, relative to the `compiled`
169
+ # directory. The directory mirrors the location of the source
170
+ # definition file (e.g. `api/v3/film.rb` -> `api/v3/film.json`).
171
+ # Definitions without a known location are written to the root of
172
+ # the compiled directory.
173
+ def compiled_relative_path(definition)
174
+ filename = compiled_filename(definition.name, definition.version)
175
+ source = definition_source_file(definition)
176
+ return Pathname.new(filename) unless source
177
+ Pathname.new(source).dirname.join(filename)
178
+ end
179
+
180
+ def definition_location(caller_line)
181
+ path, line = caller_line.split(':').take(2)
182
+ configuration.definition_paths.each do |definition_path|
183
+ if path.start_with?(definition_path.to_s)
184
+ path = Pathname.new(path).relative_path_from(definition_path)
185
+ break
186
+ end
187
+ end
188
+ [path, line].join(':')
189
+ end
190
+
61
191
  def load_definitions
62
- globbed_paths = configuration.definition_paths.map { |path| path.join('**', '*.rb') }
63
- Dir[globbed_paths.join(',')].each do |schema_file|
192
+ configuration.definition_paths.map! { |p| Pathname.new(p) }
193
+ globbed_paths = configuration.definition_paths.map { |path| path.join('**', '*.rb').to_s }
194
+ Dir[*globbed_paths].each do |schema_file|
64
195
  require schema_file
65
196
  end
66
197
  end
data/schema-test.gemspec CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency 'json_schemer'
40
40
 
41
41
  spec.add_development_dependency "bundler", "~> 2"
42
- spec.add_development_dependency "rake", "~> 10.0"
42
+ spec.add_development_dependency "rake", "~> 13.0"
43
43
  spec.add_development_dependency "rspec", "~> 3.0"
44
44
  spec.add_development_dependency "byebug"
45
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema-test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Adam
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-01 00:00:00.000000000 Z
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '13.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '13.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -101,6 +101,7 @@ executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
+ - ".github/dependabot.yml"
104
105
  - ".github/workflows/tests.yml"
105
106
  - ".gitignore"
106
107
  - ".rspec"
@@ -113,13 +114,14 @@ files:
113
114
  - bin/console
114
115
  - bin/setup
115
116
  - lib/schema_test.rb
117
+ - lib/schema_test/collapser.rb
116
118
  - lib/schema_test/collection.rb
117
119
  - lib/schema_test/configuration.rb
118
120
  - lib/schema_test/definition.rb
121
+ - lib/schema_test/fingerprint_rewriter.rb
119
122
  - lib/schema_test/minitest.rb
120
123
  - lib/schema_test/property.rb
121
124
  - lib/schema_test/rewriter.rb
122
- - lib/schema_test/test_helper.rb
123
125
  - lib/schema_test/validator.rb
124
126
  - lib/schema_test/version.rb
125
127
  - schema-test.gemspec
@@ -146,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
148
  - !ruby/object:Gem::Version
147
149
  version: '0'
148
150
  requirements: []
149
- rubygems_version: 3.1.4
151
+ rubygems_version: 3.5.22
150
152
  signing_key:
151
153
  specification_version: 4
152
154
  summary: API testing against declarative schemas
@@ -1,59 +0,0 @@
1
- module SchemaTest
2
- class TestHelper
3
- def assert_api_schema(name, version:, structure: nil)
4
- install_asset_api_expansion_hook
5
-
6
- definition = ApiSchema::Definition.find(name, version)
7
- raise "Unknown definition #{name}, version: #{version}" unless definition.present?
8
-
9
- expected_structure = definition.as_structure
10
-
11
- if structure != expected_structure
12
- if ENV['CI']
13
- flunk "Outdated API schema assertion at #{caller[0]}"
14
- else
15
- queue_write_expanded_assert_api_call(caller[0], __method__, name, version, expected_structure)
16
- end
17
- end
18
-
19
- assert_json_response_structure(*expected_structure)
20
- end
21
-
22
- private
23
-
24
- @@__api_schema_calls_for_expansion = {}
25
- @@__api_schema_expansion_hook_installed = false
26
-
27
- def queue_write_expanded_assert_api_call(call_site, method, name, version, expected_structure)
28
- file, line = call_site.split(':')
29
- line_index = line.to_i.pred
30
-
31
- @@__api_schema_calls_for_expansion[file] ||= []
32
- @@__api_schema_calls_for_expansion[file] << [line_index, method, name, version, expected_structure]
33
- end
34
-
35
- def install_asset_api_expansion_hook
36
- return if @@__api_schema_expansion_hook_installed
37
- at_exit { expand_assert_api_calls }
38
- @@__api_schema_expansion_hook_installed = true
39
- end
40
-
41
- def expand_assert_api_calls
42
- @@__api_schema_calls_for_expansion.each do |file, line_indexes_with_structures|
43
- rewriter = SchemaTest::Rewriter.new(File.read(file), line_indexes_with_structures)
44
- File.open(file, 'w') { f.puts rewriter.output }
45
- end
46
- end
47
- end
48
- end
49
-
50
- if const_defined?(Rails)
51
- ActionController::TestCase.send(:include, SchemaTest::TestHelper)
52
- ActionDispatch::IntegrationTest.send(:include, SchemaTest::TestHelper)
53
-
54
- SchemaTest.definition_paths << Rails.root.join('test', 'schema_definitions', '**', '*.rb')
55
- SchemaTest.definition_paths << Rails.root.join('spec', 'schema_definitions', '**', '*.rb')
56
-
57
- SchemaTest.load_definitions
58
- end
59
-