minitest-smartdiff 0.1.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: abc313dca9e7330ddc1104fb2ae691b5cb7725fe02b36291b1a83ad9e69b2b33
4
+ data.tar.gz: 5d5bc8a66463630af71e9f7ecfd33a0c69f84adb479ddc5df04abf06873f2d80
5
+ SHA512:
6
+ metadata.gz: a401a5d70f5ad5388aaf9c02cacb1b909341b28e7ab6e66cefff0756c8e53dba9b35f6ce702cc75b435e43a874cd537a7b700b2b73ae2c697a9b70fe5074a03b
7
+ data.tar.gz: c0d7f89296dcd2d32cbcd0cf248764e776cd519fca5cd5b29b5b84eaa3bd37de676eb9a800d47419eb6dac41a39b949879373394cd77e1aff09547b694bf27c5
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.10
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in minitest-smartdiff.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,56 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ minitest-smartdiff (0.1.0)
5
+ ruby-openai (~> 7.0)
6
+ xxhash (~> 0.5)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ debug (1.9.2)
12
+ irb (~> 1.10)
13
+ reline (>= 0.3.8)
14
+ event_stream_parser (1.0.0)
15
+ faraday (2.9.0)
16
+ faraday-net_http (>= 2.0, < 3.2)
17
+ faraday-multipart (1.0.4)
18
+ multipart-post (~> 2)
19
+ faraday-net_http (3.1.0)
20
+ net-http
21
+ io-console (0.7.2)
22
+ irb (1.13.0)
23
+ rdoc (>= 4.0.0)
24
+ reline (>= 0.4.2)
25
+ minitest (5.22.3)
26
+ multipart-post (2.4.0)
27
+ net-http (0.4.1)
28
+ uri
29
+ psych (5.1.2)
30
+ stringio
31
+ rake (13.2.1)
32
+ rdoc (6.6.3.1)
33
+ psych (>= 4.0.0)
34
+ reline (0.5.4)
35
+ io-console (~> 0.5)
36
+ ruby-openai (7.0.1)
37
+ event_stream_parser (>= 0.3.0, < 2.0.0)
38
+ faraday (>= 1)
39
+ faraday-multipart (>= 1)
40
+ stringio (3.1.0)
41
+ uri (0.13.0)
42
+ xxhash (0.5.0)
43
+
44
+ PLATFORMS
45
+ arm64-darwin-23
46
+ ruby
47
+
48
+ DEPENDENCIES
49
+ bundler (~> 2.5)
50
+ debug (~> 1.9)
51
+ minitest (~> 5.0)
52
+ minitest-smartdiff!
53
+ rake (~> 13.2)
54
+
55
+ BUNDLED WITH
56
+ 2.5.3
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Minitest::Smartdiff
2
+
3
+ Have you ever found yourself staring cross-eyed and bewildered at walls of diffs that are **absolutely the same damn it**.
4
+
5
+ Well, it's the future, and you don't have to do that anymore. Let the endlessly patient Robot go looking for the needle in the needle stack.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'minitest-smartdiff'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install minitest-smartdiff
22
+
23
+ ## Usage
24
+
25
+ In your `test_helper.rb` file:
26
+
27
+ ```ruby
28
+ require 'minitest-smartdiff'
29
+ ```
30
+
31
+ And in your test file:
32
+
33
+ ```ruby
34
+ class MyTestThatProducesAnAnnoyingDiff < Minitest::Test
35
+ include Minitest::Smartdiff
36
+
37
+ def test_that_produces_an_annoying_obscure_diff
38
+ some_result = my_method()
39
+
40
+ smart_diff do
41
+ assert_equal "Mayonnaise is delicious", "Mayonnaise is delicioous"
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
47
+ Alternatively - you can call `smart_diff_on` and `smart_diff_off` to turn the functionality on/off without using a block.
48
+
49
+ Smartdiff is smart enough not to ask the LLM for the difference if the assertion passes, so you don't accrue OpenAI costs for passing tests. However - you should consider this primarily a debugging tool - use at high volume at your own risk.
50
+
51
+ You can configure the OpenAI client like so:
52
+
53
+ ```ruby
54
+ class MyTestThatProducesAnAnnoyingDiff < Minitest::Test
55
+ include Minitest::Smartdiff
56
+
57
+ openai({
58
+ access_token: ENV['OPENAI_KEY']
59
+ })
60
+
61
+ model('gpt-3.5-turbo')
62
+
63
+ prompt(<<~ERB
64
+ You are a differ - you diff things. Diff this:
65
+ <%= expected %>
66
+ <%= actual %>
67
+ <%= mode %> is either `:json`, `:object` or `:text`
68
+ ERB
69
+ )
70
+
71
+
72
+ def test_that_produces_an_annoying_obscure_diff
73
+ some_result = my_method()
74
+
75
+ smart_diff do
76
+ assert_equal "Mayonnaise is delicious", "Mayonnaise is delicioous"
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Development
83
+
84
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
85
+
86
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
87
+
88
+ ## Contributing
89
+
90
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stephenprater/minitest-smartdiff.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "minitest/smartdiff"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ module Minitest
2
+ module Smartdiff
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,177 @@
1
+ require "minitest/smartdiff/version"
2
+ require "minitest/assertions"
3
+ require "xxhash"
4
+ require "openai"
5
+ require "forwardable"
6
+ require "erb"
7
+
8
+ module Minitest
9
+ module Smartdiff
10
+ class Error < StandardError; end
11
+
12
+ extend Forwardable
13
+
14
+ def self.included(base)
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ def smart_diff_off
19
+ @smart_diff = false
20
+ end
21
+
22
+ def smart_diff_on
23
+ @smart_diff = true
24
+ end
25
+
26
+ def smart_diff
27
+ raise ArgumentError, "Expected a block but none was given" unless block_given?
28
+ smart_diff_on
29
+ yield
30
+ ensure
31
+ smart_diff_off
32
+ end
33
+
34
+ def_delegators :"self.class", :openai, :prompt, :model, :valid_json?, :smart_diffable?
35
+
36
+ module ClassMethods
37
+ def openai(config = {})
38
+ @openai ||= OpenAI::Client.new(
39
+ {
40
+ organization_id: "",
41
+ log_errors: true,
42
+ access_token: ENV['OPENAI_KEY'],
43
+ uri_base: ENV['OPENAI_URI_BASE'],
44
+ }.merge(config)
45
+ )
46
+ end
47
+
48
+ def prompt(string = nil)
49
+ @prompt ||= string
50
+ @prompt ||= ERB.new <<~DEFAULT
51
+ You are Minitest::Smartdiff - the smartest differ that ever existed.
52
+ Your task is to find subtle but important differences in two pieces of data,
53
+ the expected, and the actual. When you find the difference describe the
54
+ difference as precisely as possible, and provide samples from the provided
55
+ input.
56
+
57
+ Be as succinct as possible, only point out the differences in the two strings.
58
+
59
+ Example Output
60
+ <% if mode == :json || mode == :object %>
61
+ Make sure to provide JSONPath to the location of the differences.
62
+
63
+ Expected:
64
+ {
65
+ usage: {
66
+ prompt_tokens: 9
67
+ }
68
+ }
69
+
70
+ Actual:
71
+ {
72
+ usage: {
73
+ prompt_tokens: 11
74
+ }
75
+ }
76
+
77
+ 1. In the usage object, the number of tokens used is different:
78
+ - JSONPath: $.usage.prompt_tokens
79
+ - Expected: 9
80
+ - Actual: 11
81
+ <% else %>
82
+ Make sure to provide a concise description of the surrounding context for the differences.
83
+
84
+ Expected:
85
+ The quick brown dog jumped over the lazy red fox.
86
+
87
+ Actual:
88
+ The quick red dog jumped over the lazy brown fox.
89
+
90
+ 1. The word "brown" in the expected text describes the dog, but in the actual text the word "red" is used.
91
+ 2. The word "red" in the expected text describes the fox, is in the actual text the word "brown" is used.
92
+ <% end %>
93
+
94
+ This is very important to my career, I've got children to feed and a
95
+ mortgage to pay - so be very careful to only show differences.
96
+
97
+ Expected:
98
+ <%= expected %>
99
+
100
+ Actual
101
+ <%= actual %>
102
+ DEFAULT
103
+ end
104
+
105
+ def model(model = nil)
106
+ @model ||= model
107
+ @model ||= "gpt-3.5-turbo"
108
+ end
109
+
110
+ def valid_json?(str)
111
+ rep = JSON.parse(str)
112
+ rep.is_a?(Hash) || rep.is_a?(Array)
113
+ rescue => e
114
+ false
115
+ end
116
+
117
+ def smart_diffable?(exp, act)
118
+ return false if exp.class != act.class
119
+ return false unless [Hash, String, Array].include?(exp.class)
120
+
121
+ if exp.is_a?(String)
122
+ if valid_json?(exp) && valid_json?(act)
123
+ :json
124
+ else
125
+ :text
126
+ end
127
+ else
128
+ :object
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ module Assertions
135
+ alias_method :old_diff, :diff
136
+
137
+ def smart_diff(exp, act)
138
+ return old_diff(exp,act) unless @smart_diff
139
+
140
+ mode = smart_diffable?(exp, act)
141
+
142
+ return old_diff(exp, act) unless mode
143
+
144
+ expected, actual = if mode == :object
145
+ [exp.to_json, act.to_json]
146
+ else
147
+ [exp, act]
148
+ end
149
+
150
+ params = {
151
+ parameters: {
152
+ messages: [
153
+ {
154
+ role: "user",
155
+ content: prompt.result_with_hash({
156
+ expected:,
157
+ actual:,
158
+ mode:
159
+ }),
160
+ },
161
+ ],
162
+ model: model,
163
+ temperature: 0.00000000000001,
164
+ seed: XXhash.xxh32(expected, actual)
165
+ }
166
+ }
167
+
168
+ completion = openai.chat(**params)
169
+ completion.dig("choices",0,"message","content") + "\n"
170
+ rescue => e
171
+ warn "Error talking to OpenAI: #{e.message} will fallback to basic diff"
172
+ old_diff(exp,act)
173
+ end
174
+
175
+ alias_method :diff, :smart_diff
176
+ end
177
+ end
@@ -0,0 +1,33 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "minitest/smartdiff/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "minitest-smartdiff"
8
+ spec.version = Minitest::Smartdiff::VERSION
9
+ spec.authors = ["Stephen Prater", "Jeremy Cobb"]
10
+ spec.email = ["me@stephenprater.com"]
11
+
12
+ spec.summary = %q{Diffs are hard, make the robot do it.}
13
+ spec.description = %q{Diff between expected and actual with GPT}
14
+ spec.homepage = "https://github.com/stephenprater/minitest-smartdiff"
15
+ spec.license = "MIT"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "ruby-openai", "~> 7.0"
27
+ spec.add_dependency "xxhash", "~> 0.5"
28
+
29
+ spec.add_development_dependency "bundler", "~> 2.5"
30
+ spec.add_development_dependency "rake", "~> 13.2"
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+ spec.add_development_dependency "debug", "~> 1.9"
33
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest-smartdiff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Prater
8
+ - Jeremy Cobb
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-05-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-openai
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '7.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '7.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: xxhash
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.5'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.5'
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.5'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '13.2'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '13.2'
70
+ - !ruby/object:Gem::Dependency
71
+ name: minitest
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '5.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '5.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: debug
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.9'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.9'
98
+ description: Diff between expected and actual with GPT
99
+ email:
100
+ - me@stephenprater.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - Gemfile.lock
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - lib/minitest/smartdiff.rb
114
+ - lib/minitest/smartdiff/version.rb
115
+ - minitest-smartdiff.gemspec
116
+ homepage: https://github.com/stephenprater/minitest-smartdiff
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.5.3
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Diffs are hard, make the robot do it.
139
+ test_files: []