diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 39d7329..8996a89 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -32,7 +32,19 @@ jobs: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-args: --without development - - name: Run Rubocop - run: bundle exec rubocop - name: Quickdraw tests - run: bundle exec qt + run: bundle exec qt -t 1 + rubocop: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.3'] + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run RuboCop + run: bundle exec rubocop diff --git a/.gitignore b/.gitignore index b82c8cc..4a3cda5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .rubocop-* .DS_Store .covered.db +Gemfile.lock diff --git a/Gemfile b/Gemfile index 45b1cef..2f11e94 100644 --- a/Gemfile +++ b/Gemfile @@ -5,10 +5,13 @@ source "https://rubygems.org" # Specify your gem's dependencies in quickdraw.gemspec gemspec -group :development do - gem "rubocop" - gem "rspec-expectations" - gem "minitest" - gem "sus" - gem "covered" +gem "rspec-expectations" + +if RUBY_ENGINE == "ruby" + group :development do + gem "rubocop" + gem "minitest" + gem "sus" + gem "covered" + end end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 197263e..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,73 +0,0 @@ -PATH - remote: . - specs: - quickdraw (0.1.0) - -GEM - remote: https://rubygems.org/ - specs: - ast (2.4.2) - console (1.23.6) - fiber-annotation - fiber-local - json - covered (0.25.1) - console (~> 1.0) - msgpack (~> 1.0) - diff-lcs (1.5.1) - fiber-annotation (0.2.0) - fiber-local (1.0.0) - json (2.7.2) - json (2.7.2-java) - language_server-protocol (3.17.0.3) - minitest (5.22.3) - msgpack (1.7.2) - msgpack (1.7.2-java) - parallel (1.25.1) - parser (3.3.3.0) - ast (~> 2.4.1) - racc - racc (1.8.0) - racc (1.8.0-java) - rainbow (3.1.1) - regexp_parser (2.9.2) - rexml (3.3.0) - strscan - rspec-expectations (3.13.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-support (3.13.1) - rubocop (1.64.1) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) - ruby-progressbar (1.13.0) - strscan (3.1.0) - strscan (3.1.0-java) - sus (0.24.6) - unicode-display_width (2.5.0) - -PLATFORMS - arm64-darwin - universal-java-11 - x86_64-linux - -DEPENDENCIES - covered - minitest - quickdraw! - rspec-expectations - rubocop - sus - -BUNDLED WITH - 2.4.6 diff --git a/lib/quickdraw.rb b/lib/quickdraw.rb index 85e3e57..dbb0720 100644 --- a/lib/quickdraw.rb +++ b/lib/quickdraw.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require "concurrent/map" +require "concurrent/array" +require "concurrent/set" + module Quickdraw autoload :ArgumentError, "quickdraw/errors/argument_error" autoload :CLI, "quickdraw/cli" @@ -26,6 +30,7 @@ module Quickdraw Null = Object.new.freeze Error = Module.new Config = Configuration.new + MATCHERS = Concurrent::Map.new def self.configure(&) yield Config diff --git a/lib/quickdraw/concurrent_array.rb b/lib/quickdraw/concurrent_array.rb deleted file mode 100644 index f63f2e4..0000000 --- a/lib/quickdraw/concurrent_array.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class Quickdraw::ConcurrentArray - def initialize - @mutex = Mutex.new - @array = [] - end - - def <<(element) - @mutex.synchronize { @array << element } - end - - def to_a - @array.dup - end - - def size - @array.size - end -end diff --git a/lib/quickdraw/context.rb b/lib/quickdraw/context.rb index 12835ef..4a640f6 100644 --- a/lib/quickdraw/context.rb +++ b/lib/quickdraw/context.rb @@ -9,6 +9,8 @@ def test(name = nil, skip: false, &block) end def use(*new_matchers) + matchers = self.matchers + i = 0 number_of_new_matchers = new_matchers.size @@ -19,10 +21,12 @@ def use(*new_matchers) end def matchers - @matchers ||= if superclass < Quickdraw::Context - superclass.matchers.dup - else - Set[] + Quickdraw::MATCHERS.fetch_or_store(self) do + if superclass < Quickdraw::Context + superclass.matchers.dup + else + Concurrent::Set.new + end end end @@ -39,7 +43,7 @@ def initialize(name, skip, runner, matchers) @runner = runner @matchers = matchers - @expectations = [] + @expectations = Concurrent::Array.new end def expect(value = Quickdraw::Null, &block) diff --git a/lib/quickdraw/map.rb b/lib/quickdraw/map.rb deleted file mode 100644 index 7b6e953..0000000 --- a/lib/quickdraw/map.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Quickdraw::Map - include Enumerable - - def initialize - @hash = {} - @mutex = Mutex.new - end - - def [](value) - @hash[value] - end - - def []=(key, value) - @mutex.synchronize do - @hash[key] = value - end - end - - def each(&) - @hash.each(&) - end - - def clear - @mutex.synchronize do - @hash.clear - end - end -end diff --git a/lib/quickdraw/registry.rb b/lib/quickdraw/registry.rb index 062158b..47f8836 100644 --- a/lib/quickdraw/registry.rb +++ b/lib/quickdraw/registry.rb @@ -2,9 +2,9 @@ class Quickdraw::Registry def initialize - @registered_matchers = Quickdraw::Map.new - @type_matchers = Quickdraw::Map.new - @shapes = Quickdraw::Map.new + @registered_matchers = Concurrent::Map.new + @type_matchers = Concurrent::Map.new + @shapes = Concurrent::Map.new end # Register a new matcher for the given types. @@ -32,15 +32,19 @@ def expectation_for(value, matchers: nil) # A "shape" is a specialised Expectation class that includes the given matchers. It's cached against the list of matchers. def shape_for(matchers) - @shapes[matchers] ||= Class.new(Quickdraw::Expectation) do - matchers.each { |m| include m } - freeze + @shapes.fetch_or_store(matchers) do + Class.new(Quickdraw::Expectation) do + matchers.each { |m| include m } + freeze + end end end # Given a value, find all the matchers that match it. This is cached against the class of the value. def matchers_for(value) - @type_matchers[value.class] ||= slowly_find_matchers_for(value) + @type_matchers.fetch_or_store(value.class) do + slowly_find_matchers_for(value) + end end # If the above has a cache miss, we'll need to find the correct matchers slowly and then cache them. diff --git a/lib/quickdraw/run.rb b/lib/quickdraw/run.rb index 77831ab..a19d32e 100644 --- a/lib/quickdraw/run.rb +++ b/lib/quickdraw/run.rb @@ -72,7 +72,7 @@ def load_tests end def batch_tests - batches = Array.new([@processes, @tests.size].min) { Quickdraw::Queue.new } + batches = Array.new([@processes, @tests.size].min) { Concurrent::Array.new } number_of_tests = @tests.size i = 0 @@ -111,7 +111,7 @@ def fork_processes end def run_inline - queue = Quickdraw::Queue.new + queue = Concurrent::Array.new i = 0 number_of_tests = @tests.size diff --git a/lib/quickdraw/runner.rb b/lib/quickdraw/runner.rb index db98d7c..dad6c12 100644 --- a/lib/quickdraw/runner.rb +++ b/lib/quickdraw/runner.rb @@ -7,8 +7,8 @@ def initialize(queue:, threads:) @queue = queue @threads = threads - @failures = Quickdraw::ConcurrentArray.new - @successes = Quickdraw::ConcurrentArray.new + @failures = Concurrent::Array.new + @successes = Concurrent::Array.new end def call @@ -47,8 +47,13 @@ def failure! private def drain_queue - @queue.drain { |(name, skip, test, context)| + queue = @queue + next_item = queue.shift + + while next_item + name, skip, test, context = next_item context.run_test(name, skip, self, &test) - } + next_item = queue.shift + end end end diff --git a/quickdraw.gemspec b/quickdraw.gemspec index 43e03cc..b5c6ad8 100644 --- a/quickdraw.gemspec +++ b/quickdraw.gemspec @@ -30,6 +30,8 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.add_runtime_dependency "concurrent-ruby" + # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html spec.metadata["rubygems_mfa_required"] = "true"