diff --git a/.gem_release.yml b/.gem_release.yml new file mode 100644 index 0000000..3dcdce8 --- /dev/null +++ b/.gem_release.yml @@ -0,0 +1,3 @@ +bump: + file: lib/imatcher/version.rb + skip_ci: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..404079b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: palkan + +--- + +## What did you do? + +## What did you expect to happen? + +## What actually happened? + +## Additional context + +## Environment + +**Ruby Version:** + +**Framework Version (Rails, whatever):** + +**Imatcher Version:** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bf78898 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: palkan + +--- + +## Is your feature request related to a problem? Please describe. + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Describe the solution you'd like + +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered + +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context + +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..26447f4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + +## What is the purpose of this pull request? + + + +## What changes did you make? (overview) + +## Is there anything you'd like reviewers to focus on? + +## Checklist + +- [ ] I've added tests for this change +- [ ] I've added a Changelog entry +- [ ] I've updated a documentation diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml new file mode 100644 index 0000000..691e5dc --- /dev/null +++ b/.github/workflows/docs-lint.yml @@ -0,0 +1,61 @@ +name: Lint Docs + +on: + push: + branches: + - master + paths: + - "*.md" + - "**/*.md" + pull_request: + paths: + - "*.md" + - "**/*.md" + +jobs: + rubocop: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + - name: Lint Markdown files with RuboCop + run: | + gem install bundler + bundle install --gemfile gemfiles/rubocop.gemfile --jobs 4 --retry 3 + bundle exec --gemfile gemfiles/rubocop.gemfile rubocop -c .rubocop-md.yml + forspell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Hunspell + run: | + sudo apt-get install hunspell + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + - name: Cache installed gems + uses: actions/cache@v1 + with: + path: /home/runner/.rubies/ruby-2.7.0/lib/ruby/gems/2.7.0 + key: gems-cache-${{ runner.os }} + - name: Install Forspell + run: gem install forspell + - name: Run Forspell + run: forspell *.md .github/**/*.md + liche: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: 1.13.x + - name: Run liche + run: | + export PATH=$PATH:$(go env GOPATH)/bin + go get -u github.com/raviqqe/liche + liche README.md CHANGELOG.md diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml new file mode 100644 index 0000000..e94bf34 --- /dev/null +++ b/.github/workflows/rspec.yml @@ -0,0 +1,28 @@ +name: Build + +on: + push: + branches: + - master + pull_request: + +jobs: + rspec: + runs-on: ubuntu-latest + env: + BUNDLE_JOBS: 4 + BUNDLE_RETRY: 3 + CI: true + strategy: + fail-fast: false + matrix: + ruby: ["2.6", "2.7", "3.0", "3.1"] + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run RSpec + run: | + bundle exec rspec diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 0000000..8890ba8 --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,21 @@ +name: Lint Ruby + +on: + push: + branches: + - master + pull_request: + +jobs: + rubocop: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + - name: Lint Ruby code with RuboCop + run: | + gem install bundler + bundle install --gemfile gemfiles/rubocop.gemfile --jobs 4 --retry 3 + bundle exec --gemfile gemfiles/rubocop.gemfile rubocop diff --git a/.gitignore b/.gitignore index 0a05a92..1ac8339 100644 --- a/.gitignore +++ b/.gitignore @@ -23,14 +23,21 @@ .settings .tmproj Thumbs.db + +.bundle/ +log/*.log +pkg/ +spec/dummy/db/*.sqlite3 +spec/dummy/db/*.sqlite3-journal +spec/dummy/tmp/ + +Gemfile.lock +Gemfile.local +.rspec +.ruby-version *.gem -/.bundle/ -/.yardoc -/Gemfile.lock -/_yardoc/ -/coverage/ -/doc/ -/pkg/ -/spec/reports/ -/tmp/ +tmp/ +.rbnext/ + +gemfiles/*.lock diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..99f6126 --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +rules "~MD013", "~MD033", "~MD029", "~MD034" diff --git a/.rspec b/.rspec index 8c18f1a..d26d193 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,2 @@ ---format documentation +-f d --color diff --git a/.rubocop-md.yml b/.rubocop-md.yml new file mode 100644 index 0000000..6bc0086 --- /dev/null +++ b/.rubocop-md.yml @@ -0,0 +1,8 @@ +inherit_from: ".rubocop.yml" + +require: + - rubocop-md + +AllCops: + Include: + - '**/*.md' diff --git a/.rubocop.yml b/.rubocop.yml index 19a62af..3e4a336 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,48 +1,25 @@ -AllCops: - # Include gemspec and Rakefile - Include: - - 'lib/**/*.rb' - - 'lib/**/*.rake' - - 'spec/**/*.rb' - Exclude: - - 'bin/**/*' - - 'spec/dummy/**/*' - Rails: - Enabled: false - DisplayCopNames: true - StyleGuideCopsOnly: false - -Style/AccessorMethodName: - Enabled: false - -Style/TrivialAccessors: - Enabled: false - -Style/Documentation: - Exclude: - - 'spec/**/*.rb' +require: + - standard/cop/block_single_line_braces -Style/StringLiterals: - Enabled: false - -Style/SpaceInsideStringInterpolation: - EnforcedStyle: no_space +inherit_gem: + standard: config/base.yml -Style/BlockDelimiters: +AllCops: Exclude: - - 'spec/**/*.rb' - -Style/ParallelAssignment: - Enabled: false + - 'bin/*' + - 'tmp/**/*' + - 'Gemfile' + - 'vendor/**/*' + - 'gemfiles/**/*' + - 'lib/.rbnext/**/*' + - 'lib/generators/**/templates/*.rb' + - '.github/**/*' + DisplayCopNames: true + SuggestExtensions: false + TargetRubyVersion: 2.6 -Lint/AmbiguousRegexpLiteral: +Standard/BlockSingleLineBraces: Enabled: false -Metrics/MethodLength: - Exclude: - - 'spec/**/*.rb' - -Metrics/LineLength: - max: 100 - Exclude: - - 'spec/**/*.rb' +Style/FrozenStringLiteralComment: + Enabled: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c9d409b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: ruby -cache: bundler -rvm: - - 2.2 - - ruby-head - - jruby-head - -notifications: - email: false - -before_install: - - gem install bundler - -script: bundle exec rake diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4f3530e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change log + +## master + +- Add `lower_threshold:` option to specify the lower bound for the expected difference. ([@palkan][]) + +- Ruby 3.1 compatibility. + +- Drop JRuby support (for now). + +[@palkan]: https://github.com/palkan diff --git a/Gemfile b/Gemfile index cd96734..d8780c3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,17 @@ -source 'https://rubygems.org' +# frozen_string_literal: true -gem "rake", "~> 10.0" -gem "rspec", "~> 3.0" +source "https://rubygems.org" -if RUBY_PLATFORM =~ /java/ - gem "chunky_png", "~> 1.3.5" -else - gem "oily_png", "~> 1.2" -end +gem "pry-byebug", platform: :mri + +gem "oily_png" + +gemspec + +eval_gemfile "gemfiles/rubocop.gemfile" -gem 'pry-byebug' if RUBY_VERSION >= "2.0.0" && RUBY_PLATFORM != 'java' -local_gemfile = 'Gemfile.local' +local_gemfile = "#{File.dirname(__FILE__)}/Gemfile.local" if File.exist?(local_gemfile) - eval(File.read(local_gemfile)) # rubocop:disable Lint/Eval + eval(File.read(local_gemfile)) # rubocop:disable Security/Eval end diff --git a/LICENSE.txt b/LICENSE.txt index 940a965..829954c 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,22 @@ -The MIT License (MIT) +Copyright (c) 2022 Vladimir Dementyev -Copyright (c) 2016 palkan +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index de7a69e..19e31c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Gem Version](https://badge.fury.io/rb/imatcher.svg)](https://rubygems.org/gems/imatcher) [![Build Status](https://travis-ci.org/teachbase/imatcher.svg?branch=master)](https://travis-ci.org/teachbase/imatcher) +[![Gem Version](https://badge.fury.io/rb/imatcher.svg)](https://rubygems.org/gems/imatcher) +[![Build](https://github.com/teachbase/imatcher/workflows/Build/badge.svg)](https://github.com/teachbase/imatcher/actions) # Imatcher @@ -10,16 +11,19 @@ This is an utility library for image regression testing. Add this line to your application's Gemfile: ```ruby -gem 'imatcher' +gem "imatcher" ``` -And then execute: +Or adding to your project: - $ bundle - -Or install it yourself as: - - $ gem install imatcher +```ruby +# my-cool-gem.gemspec +Gem::Specification.new do |spec| + # ... + spec.add_dependency "imatcher" + # ... +end +``` Additionally, you may want to install [oily_png](https://github.com/wvanbergen/oily_png) to improve performance when using MRI. Just install it globally or add to your Gemfile. @@ -66,7 +70,11 @@ cmp.mode #=> Imatcher::Modes::RGB cmp = Imatcher::Matcher.new threshold: 0.05 cmp.threshold #=> 0.05 -# create zero-tolerance grayscale matcher +# or with a lower threshold (in case you want to test that there is some difference) +cmp = Imatcher::Matcher.new lower_threshold: 0.01 +cmp.lower_threshold #=> 0.01 + +# create zero-tolerance grayscale matcher cmp = Imatcher::Matcher.new mode: :grayscale, tolerance: 0 cmp.mode #=> Imatcher::Modes::Grayscale @@ -83,7 +91,7 @@ res.difference_image #=> Imatcher::Image res.difference_image.save(new_path) # without explicit matcher -res = Imatcher.compare(path_1, path_2, options) +res = Imatcher.compare(path_1, path_2, options) # equals to res = Imatcher::Matcher.new(options).compare(path_1, path_2) @@ -113,4 +121,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/teachb ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). - diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..1176d01 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,43 @@ +# How to release a gem + +This document describes a process of releasing a new version of a gem. + +1. Bump version. + +```sh +git commit -m "Bump 1.." +``` + +We're (kinda) using semantic versioning: + +- Bugfixes should be released as fast as possible as patch versions. +- New features could be combined and released as minor or patch version upgrades (depending on the _size of the feature_—it's up to maintainers to decide). +- Breaking API changes should be avoided in minor and patch releases. +- Breaking dependencies changes (e.g., dropping older Ruby support) could be released in minor versions. + +How to bump a version: + +- Change the version number in `lib/imatcher/version.rb` file. +- Update the changelog (add new heading with the version name and date). +- Update the installation documentation if necessary (e.g., during minor and major updates). + +2. Push code to GitHub and make sure CI passes. + +```sh +git push +``` + +3. Release a gem. + +```sh +gem release -t +git push --tags +``` + +We use [gem-release](https://github.com/svenfuchs/gem-release) for publishing gems with a single command: + +```sh +gem release -t +``` + +Don't forget to push tags and write release notes on GitHub (if necessary). diff --git a/Rakefile b/Rakefile index e90ca67..722aa00 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,20 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) -task :default => :spec +begin + require "rubocop/rake_task" + RuboCop::RakeTask.new -task :console do - sh 'pry -r ./lib/imatcher.rb' + RuboCop::RakeTask.new("rubocop:md") do |task| + task.options << %w[-c .rubocop-md.yml] + end +rescue LoadError + task(:rubocop) {} + task("rubocop:md") {} end + +task default: %w[rubocop rubocop:md spec] diff --git a/examples/performance.rb b/examples/performance.rb index f10894c..1e0c2be 100644 --- a/examples/performance.rb +++ b/examples/performance.rb @@ -1,24 +1,26 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) -require 'benchmark/ips' -require 'imatcher' +# frozen_string_literal: true -a = Imatcher::Image.from_file(File.expand_path('../../spec/fixtures/a.png', __FILE__)) -b = Imatcher::Image.from_file(File.expand_path('../../spec/fixtures/a.png', __FILE__)) +$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) +require "benchmark/ips" +require "imatcher" + +a = Imatcher::Image.from_file(File.expand_path("../../spec/fixtures/a.png", __FILE__)) +b = Imatcher::Image.from_file(File.expand_path("../../spec/fixtures/a.png", __FILE__)) rgb = Imatcher::Matcher.new grayscale = Imatcher::Matcher.new mode: :grayscale delta = Imatcher::Matcher.new mode: :delta Benchmark.ips do |x| - x.report 'RGB' do + x.report "RGB" do rgb.compare(a, b) end - x.report 'Grayscale' do + x.report "Grayscale" do grayscale.compare(a, b) end - x.report 'Delta E' do + x.report "Delta E" do delta.compare(a, b) end @@ -26,15 +28,15 @@ end Benchmark.ips do |x| - x.report 'RGB' do + x.report "RGB" do rgb.compare(a, b).difference_image end - x.report 'Grayscale' do + x.report "Grayscale" do grayscale.compare(a, b).difference_image end - x.report 'Delta E' do + x.report "Delta E" do delta.compare(a, b).difference_image end diff --git a/forspell.dict b/forspell.dict new file mode 100644 index 0000000..6cb3c0a --- /dev/null +++ b/forspell.dict @@ -0,0 +1,9 @@ +# Format: one word per line. Empty lines and #-comments are supported too. +# If you want to add word with its forms, you can write 'word: example' (without quotes) on the line, +# where 'example' is existing word with the same possible forms (endings) as your word. +# Example: deduplicate: duplicate +assignees +Grayscale +grayscale +Imatcher +palkan diff --git a/gemfiles/rubocop.gemfile b/gemfiles/rubocop.gemfile new file mode 100644 index 0000000..aa25bbd --- /dev/null +++ b/gemfiles/rubocop.gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" do + gem "rubocop-md", "~> 1.0" + gem "standard", "~> 1.0" +end diff --git a/imatcher.gemspec b/imatcher.gemspec index 6329850..50bd49e 100644 --- a/imatcher.gemspec +++ b/imatcher.gemspec @@ -1,22 +1,31 @@ -$:.push File.expand_path("../lib", __FILE__) -require 'imatcher/version' +# frozen_string_literal: true + +require_relative "lib/imatcher/version" Gem::Specification.new do |spec| - spec.name = "imatcher" - spec.version = Imatcher::VERSION - spec.authors = ["palkan"] - spec.email = ["dementiev.vm@gmail.com"] - spec.summary = "Image comparison lib" - spec.description = "Image comparison lib built on top of ChunkyPNG" - spec.homepage = "http://github.com/teachbase/imatcher" - spec.license = "MIT" + spec.name = "imatcher" + spec.version = Imatcher::VERSION + spec.authors = ["palkan"] + spec.email = ["dementiev.vm@gmail.com"] + spec.summary = "Image comparison lib" + spec.description = "Image comparison lib built on top of ChunkyPNG" + spec.homepage = "http://github.com/teachbase/imatcher" + spec.license = "MIT" + spec.metadata = { + "bug_tracker_uri" => "http://github.com/palkan/imatcher/issues", + "changelog_uri" => "https://github.com/palkan/imatcher/blob/master/CHANGELOG.md", + "documentation_uri" => "http://github.com/palkan/imatcher", + "homepage_uri" => "http://github.com/palkan/imatcher", + "source_code_uri" => "http://github.com/palkan/imatcher" + } - spec.files = `git ls-files`.split($/) + spec.files = Dir.glob("lib/**/*") + Dir.glob("bin/**/*") + %w[README.md LICENSE.txt CHANGELOG.md] spec.require_paths = ["lib"] - spec.add_dependency "chunky_png", "~> 1.3.5" + spec.required_ruby_version = ">= 2.6" + + spec.add_dependency "chunky_png" - spec.add_development_dependency "simplecov", ">= 0.3.8" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "rake", ">= 13.0" + spec.add_development_dependency "rspec", ">= 3.9" end diff --git a/lib/imatcher.rb b/lib/imatcher.rb index fc4b20e..c22fe50 100644 --- a/lib/imatcher.rb +++ b/lib/imatcher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "imatcher/version" # Compare PNG images using different algorithms @@ -5,10 +7,10 @@ module Imatcher class SizesMismatchError < StandardError end - require 'imatcher/matcher' - require 'imatcher/color_methods' + require "imatcher/matcher" + require "imatcher/color_methods" - def self.compare(path_a, path_b, options = {}) - Matcher.new(options).compare(path_a, path_b) + def self.compare(path_a, path_b, **options) + Matcher.new(**options).compare(path_a, path_b) end end diff --git a/lib/imatcher/color_methods.rb b/lib/imatcher/color_methods.rb index 28ea853..61c4cbb 100644 --- a/lib/imatcher/color_methods.rb +++ b/lib/imatcher/color_methods.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require "chunky_png" begin - require "oily_png" unless RUBY_PLATFORM == 'java' -rescue LoadError # rubocop:disable Lint/HandleExceptions + require "oily_png" unless RUBY_PLATFORM == "java" +rescue LoadError end module Imatcher diff --git a/lib/imatcher/image.rb b/lib/imatcher/image.rb index cd8fbd5..e3e876e 100644 --- a/lib/imatcher/image.rb +++ b/lib/imatcher/image.rb @@ -1,4 +1,6 @@ -require 'imatcher/color_methods' +# frozen_string_literal: true + +require "imatcher/color_methods" module Imatcher # Extend ChunkyPNG::Image with some methods. diff --git a/lib/imatcher/matcher.rb b/lib/imatcher/matcher.rb index 932b764..b00ce83 100644 --- a/lib/imatcher/matcher.rb +++ b/lib/imatcher/matcher.rb @@ -1,47 +1,57 @@ +# frozen_string_literal: true + module Imatcher # Matcher contains information about compare mode class Matcher - require 'imatcher/image' - require 'imatcher/result' - require 'imatcher/modes' + require "imatcher/image" + require "imatcher/result" + require "imatcher/modes" MODES = { - rgb: 'RGB', - delta: 'Delta', - grayscale: 'Grayscale' + rgb: "RGB", + delta: "Delta", + grayscale: "Grayscale" }.freeze - attr_reader :threshold, :mode + attr_reader :mode - def initialize(options = {}) + def initialize(**options) mode_type = options.delete(:mode) || :rgb - fail ArgumentError, "Undefined mode: #{ mode_type }" unless MODES.keys.include?(mode_type) - @mode = Modes.const_get(MODES[mode_type]).new(options) + fail ArgumentError, "Undefined mode: #{mode_type}" unless MODES.key?(mode_type) + @mode = Modes.const_get(MODES[mode_type]).new(**options) end def compare(a, b) a = Image.from_file(a) unless a.is_a?(Image) b = Image.from_file(b) unless b.is_a?(Image) - fail SizesMismatchError, - "Size mismatch: first image size: " \ - "#{a.width}x#{a.height}, " \ - "second image size: " \ - "#{b.width}x#{b.height}" unless a.sizes_match?(b) + unless a.sizes_match?(b) + fail SizesMismatchError, + "Size mismatch: first image size: " \ + "#{a.width}x#{a.height}, " \ + "second image size: " \ + "#{b.width}x#{b.height}" + end image_area = Rectangle.new(0, 0, a.width - 1, a.height - 1) unless mode.exclude_rect.nil? - fail ArgumentError, - "Bounds must be in image" unless image_area.contains?(mode.exclude_rect) + unless image_area.contains?(mode.exclude_rect) + fail ArgumentError, + "Bounds must be in image" + end end unless mode.include_rect.nil? - fail ArgumentError, - "Bounds must be in image" unless image_area.contains?(mode.include_rect) - unless mode.exclude_rect.nil? + unless image_area.contains?(mode.include_rect) fail ArgumentError, - "Included area must contain excluded" unless mode.include_rect.contains?(mode.exclude_rect) + "Bounds must be in image" + end + unless mode.exclude_rect.nil? + unless mode.include_rect.contains?(mode.exclude_rect) + fail ArgumentError, + "Included area must contain excluded" + end end end diff --git a/lib/imatcher/modes.rb b/lib/imatcher/modes.rb index d6ac368..1d1bc35 100644 --- a/lib/imatcher/modes.rb +++ b/lib/imatcher/modes.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Imatcher module Modes # :nodoc: - require 'imatcher/modes/rgb' - require 'imatcher/modes/grayscale' - require 'imatcher/modes/delta' + require "imatcher/modes/rgb" + require "imatcher/modes/grayscale" + require "imatcher/modes/delta" end end diff --git a/lib/imatcher/modes/base.rb b/lib/imatcher/modes/base.rb index 7a4a652..55102be 100644 --- a/lib/imatcher/modes/base.rb +++ b/lib/imatcher/modes/base.rb @@ -1,16 +1,19 @@ +# frozen_string_literal: true + module Imatcher module Modes class Base # :nodoc: - require 'imatcher/rectangle' + require "imatcher/rectangle" include ColorMethods - attr_reader :result, :threshold, :bounds, :exclude_rect, :include_rect + attr_reader :result, :threshold, :lower_threshold, :bounds, :exclude_rect, :include_rect - def initialize(threshold: 0.0, exclude_rect: nil, include_rect: nil) + def initialize(threshold: 0.0, lower_threshold: 0.0, exclude_rect: nil, include_rect: nil) @include_rect = Rectangle.new(*include_rect) unless include_rect.nil? @exclude_rect = Rectangle.new(*exclude_rect) unless exclude_rect.nil? @threshold = threshold - @result = Result.new(self, threshold) + @lower_threshold = lower_threshold + @result = Result.new(self, threshold: threshold, lower_threshold: lower_threshold) end def compare(a, b) @@ -33,9 +36,9 @@ def diff(bg, diff) diff.each do |pixels_pair| pixels_diff(diff_image, *pixels_pair) end - create_diff_image(bg, diff_image). - highlight_rectangle(bounds). - highlight_rectangle(include_rect, :green) + create_diff_image(bg, diff_image) + .highlight_rectangle(bounds) + .highlight_rectangle(include_rect, :green) end def score diff --git a/lib/imatcher/modes/delta.rb b/lib/imatcher/modes/delta.rb index d261ad9..aff5942 100644 --- a/lib/imatcher/modes/delta.rb +++ b/lib/imatcher/modes/delta.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + module Imatcher module Modes # :nodoc: - require 'imatcher/modes/base' + require "imatcher/modes/base" # Compare pixels using Delta E distance. class Delta < Base attr_reader :tolerance - def initialize(options) + def initialize(**options) @tolerance = options.delete(:tolerance) || 0.01 @delta_score = 0.0 - super(options) + super(**options) end private diff --git a/lib/imatcher/modes/grayscale.rb b/lib/imatcher/modes/grayscale.rb index 53eadd8..7c0b44a 100644 --- a/lib/imatcher/modes/grayscale.rb +++ b/lib/imatcher/modes/grayscale.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module Imatcher module Modes # :nodoc: - require 'imatcher/modes/base' + require "imatcher/modes/base" # Compare pixels by alpha and brightness. # @@ -12,9 +14,9 @@ class Grayscale < Base attr_reader :tolerance - def initialize(options) + def initialize(**options) @tolerance = options.delete(:tolerance) || DEFAULT_TOLERANCE - super(options) + super(**options) end def pixels_equal?(a, b) diff --git a/lib/imatcher/modes/rgb.rb b/lib/imatcher/modes/rgb.rb index b4f19f1..3fb3139 100644 --- a/lib/imatcher/modes/rgb.rb +++ b/lib/imatcher/modes/rgb.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module Imatcher module Modes # :nodoc: - require 'imatcher/modes/base' + require "imatcher/modes/base" # Compare pixels by values. # Resulting image contains per-channel differences. diff --git a/lib/imatcher/rectangle.rb b/lib/imatcher/rectangle.rb index 5845834..8407e99 100644 --- a/lib/imatcher/rectangle.rb +++ b/lib/imatcher/rectangle.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Imatcher class Rectangle attr_accessor :left, :top, :right, :bot diff --git a/lib/imatcher/result.rb b/lib/imatcher/result.rb index 03e968b..7f6dfb8 100644 --- a/lib/imatcher/result.rb +++ b/lib/imatcher/result.rb @@ -1,13 +1,16 @@ +# frozen_string_literal: true + module Imatcher # Object containing comparison score and diff image class Result attr_accessor :score, :image - attr_reader :diff, :mode, :threshold + attr_reader :diff, :mode, :threshold, :lower_threshold - def initialize(mode, threshold) + def initialize(mode, threshold:, lower_threshold:) @score = 0.0 @diff = [] @threshold = threshold + @lower_threshold = lower_threshold @mode = mode end @@ -17,7 +20,7 @@ def difference_image # Returns true iff score less or equals to threshold def match? - score <= threshold + score <= threshold && score >= lower_threshold end end end diff --git a/lib/imatcher/version.rb b/lib/imatcher/version.rb index 59ff35c..4aae139 100644 --- a/lib/imatcher/version.rb +++ b/lib/imatcher/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Imatcher # :nodoc: - VERSION = "0.1.9".freeze + VERSION = "1.0.0-dev" end diff --git a/spec/image_spec.rb b/spec/image_spec.rb index 3466269..b7048b4 100644 --- a/spec/image_spec.rb +++ b/spec/image_spec.rb @@ -1,4 +1,6 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher::Image do describe "highlight_rectangle" do diff --git a/spec/imatcher_spec.rb b/spec/imatcher_spec.rb index 1d425fa..cfddcea 100644 --- a/spec/imatcher_spec.rb +++ b/spec/imatcher_spec.rb @@ -1,7 +1,9 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher do - it 'has a version number' do + it "has a version number" do expect(Imatcher::VERSION).not_to be nil end end diff --git a/spec/integrations/delta_spec.rb b/spec/integrations/delta_spec.rb index 82a22c0..938b0e6 100644 --- a/spec/integrations/delta_spec.rb +++ b/spec/integrations/delta_spec.rb @@ -1,11 +1,13 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher::Modes::Delta do let(:path_1) { image_path "a" } let(:path_2) { image_path "darker" } - subject { Imatcher.compare(path_1, path_2, options) } + subject { Imatcher.compare(path_1, path_2, **options) } - let(:options) { { mode: :delta } } + let(:options) { {mode: :delta} } context "with darker image" do it "score around 0.075" do @@ -13,19 +15,25 @@ end context "with custom threshold" do - subject { Imatcher.compare(path_1, path_2, options).match? } + subject { Imatcher.compare(path_1, path_2, **options).match? } context "below score" do - let(:options) { { mode: :delta, threshold: 0.01 } } + let(:options) { {mode: :delta, threshold: 0.01} } it { expect(subject).to be_falsey } end context "above score" do - let(:options) { { mode: :delta, threshold: 0.1 } } + let(:options) { {mode: :delta, threshold: 0.1} } it { expect(subject).to be_truthy } end + + context "with lower threshold" do + let(:options) { {mode: :delta, threshold: 0.1, lower_threshold: 0.09} } + + it { expect(subject).to be_falsey } + end end end @@ -41,7 +49,7 @@ end context "with high tolerance" do - let(:options) { { mode: :delta, tolerance: 0.1 } } + let(:options) { {mode: :delta, tolerance: 0.1} } it "score around 0.0038" do expect(subject.score).to be_within(0.0001).of(0.0038) diff --git a/spec/integrations/grayscale_spec.rb b/spec/integrations/grayscale_spec.rb index bf86f48..9c53212 100644 --- a/spec/integrations/grayscale_spec.rb +++ b/spec/integrations/grayscale_spec.rb @@ -1,11 +1,13 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher::Modes::Grayscale do let(:path_1) { image_path "a" } let(:path_2) { image_path "darker" } - subject { Imatcher.compare(path_1, path_2, options) } + subject { Imatcher.compare(path_1, path_2, **options) } - let(:options) { { mode: :grayscale } } + let(:options) { {mode: :grayscale} } context "darker image" do it "score around 0.95" do @@ -26,7 +28,7 @@ end context "with zero tolerance" do - let(:options) { { mode: :grayscale, tolerance: 0 } } + let(:options) { {mode: :grayscale, tolerance: 0} } context "darker image" do it "score equals to 1" do @@ -52,7 +54,7 @@ end context "with small tolerance" do - let(:options) { { mode: :grayscale, tolerance: 8 } } + let(:options) { {mode: :grayscale, tolerance: 8} } context "darker image" do it "score around 0.96" do diff --git a/spec/integrations/rgb_spec.rb b/spec/integrations/rgb_spec.rb index 168ea7c..8ab8627 100644 --- a/spec/integrations/rgb_spec.rb +++ b/spec/integrations/rgb_spec.rb @@ -1,9 +1,11 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher::Modes::RGB do let(:path_1) { image_path "a" } let(:path_2) { image_path "darker" } - subject { Imatcher.compare(path_1, path_2, options) } + subject { Imatcher.compare(path_1, path_2, **options) } let(:options) { {} } context "with darker" do @@ -25,7 +27,7 @@ end context "exclude rect" do - let(:options) { { exclude_rect: [200, 150, 275, 200] } } + let(:options) { {exclude_rect: [200, 150, 275, 200]} } let(:path_2) { image_path "a1" } it { expect(subject.difference_image).to eq Imatcher::Image.from_file(image_path("exclude")) } it { expect(subject.score).to eq 0 } @@ -38,7 +40,7 @@ end context "include rect" do - let(:options) { { include_rect: [0, 0, 100, 100] } } + let(:options) { {include_rect: [0, 0, 100, 100]} } let(:path_2) { image_path "a1" } it { expect(subject.difference_image).to eq Imatcher::Image.from_file(image_path("include")) } it { expect(subject.score).to eq 0 } diff --git a/spec/matcher_spec.rb b/spec/matcher_spec.rb index 4f38d2d..38663ae 100644 --- a/spec/matcher_spec.rb +++ b/spec/matcher_spec.rb @@ -1,8 +1,10 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher::Matcher do describe "new" do - subject { Imatcher::Matcher.new(options) } + subject { Imatcher::Matcher.new(**options) } context "without options" do let(:options) { {} } @@ -11,26 +13,27 @@ it { expect(subject.mode).to be_a Imatcher::Modes::RGB } end - context "with custom threshold" do - let(:options) { { threshold: 0.1 } } + context "with custom thresholds" do + let(:options) { {threshold: 0.1, lower_threshold: 0.04} } it { expect(subject.mode.threshold).to eq 0.1 } + it { expect(subject.mode.lower_threshold).to eq 0.04 } end context "with custom options" do - let(:options) { { mode: :grayscale, tolerance: 0 } } + let(:options) { {mode: :grayscale, tolerance: 0} } it { expect(subject.mode.tolerance).to eq 0 } end context "with custom mode" do - let(:options) { { mode: :delta } } + let(:options) { {mode: :delta} } it { expect(subject.mode).to be_a Imatcher::Modes::Delta } end context "with undefined mode" do - let(:options) { { mode: :gamma } } + let(:options) { {mode: :gamma} } it { expect { subject }.to raise_error(ArgumentError) } end @@ -40,7 +43,7 @@ let(:path_1) { image_path "very_small" } let(:path_2) { image_path "very_small" } let(:options) { {} } - subject { Imatcher.compare(path_1, path_2, options) } + subject { Imatcher.compare(path_1, path_2, **options) } it { expect(subject).to be_a Imatcher::Result } @@ -50,27 +53,27 @@ end context "with negative exclude rect bounds" do - let(:options) { { exclude_rect: [-1, -1, -1, -1] } } + let(:options) { {exclude_rect: [-1, -1, -1, -1]} } it { expect { subject }.to raise_error ArgumentError } end context "with big exclude rect bounds" do - let(:options) { { exclude_rect: [100, 100, 100, 100] } } + let(:options) { {exclude_rect: [100, 100, 100, 100]} } it { expect { subject }.to raise_error ArgumentError } end context "with negative include rect bounds" do - let(:options) { { include_rect: [-1, -1, -1, -1] } } + let(:options) { {include_rect: [-1, -1, -1, -1]} } it { expect { subject }.to raise_error ArgumentError } end context "with big include rect bounds" do - let(:options) { { include_rect: [100, 100, 100, 100] } } + let(:options) { {include_rect: [100, 100, 100, 100]} } it { expect { subject }.to raise_error ArgumentError } end context "with wrong include and exclude rects combination" do - let(:options) { { include_rect: [1, 1, 2, 2], exclude_rect: [0, 0, 1, 1] } } + let(:options) { {include_rect: [1, 1, 2, 2], exclude_rect: [0, 0, 1, 1]} } it { expect { subject }.to raise_error ArgumentError } end end diff --git a/spec/rectangle_spec.rb b/spec/rectangle_spec.rb index 9292092..65f2792 100644 --- a/spec/rectangle_spec.rb +++ b/spec/rectangle_spec.rb @@ -1,9 +1,11 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "spec_helper" describe Imatcher::Rectangle do let(:rect) { described_class.new(0, 0, 9, 9) } - describe 'area' do + describe "area" do subject { rect.area } it { expect(subject).to eq 100 } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d7eb8c8..0ee1876 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,20 +1,23 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +# frozen_string_literal: true -require 'rspec' -require 'pry-byebug' if RUBY_VERSION >= "2.0.0" && RUBY_PLATFORM != 'java' - -if ENV['COVER'] - require 'simplecov' - SimpleCov.root File.join(File.dirname(__FILE__), '..') - SimpleCov.start +begin + require "pry-byebug" +rescue LoadError end -require 'imatcher' +require "imatcher" -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } RSpec.configure do |config| config.mock_with :rspec + + config.example_status_persistence_file_path = "tmp/rspec_examples.txt" + config.filter_run :focus + config.run_all_when_everything_filtered = true + + config.order = :random + Kernel.srand config.seed end def image_path(name)