thread-order-1.1.0/0000755000175000017500000000000012537102161013445 5ustar boutilboutilthread-order-1.1.0/metadata.yml0000644000175000017500000000314212537102161015750 0ustar boutilboutil--- !ruby/object:Gem::Specification name: thread_order version: !ruby/object:Gem::Version version: 1.1.0 platform: ruby authors: - Josh Cheek autorequire: bindir: bin cert_chain: [] date: 2015-03-16 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '3.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '3.0' description: Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2, rbx, jruby). email: josh.cheek@gmail.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - .travis.yml - Gemfile - License.txt - Readme.md - lib/thread_order.rb - lib/thread_order/mutex.rb - lib/thread_order/version.rb - spec/run - spec/thread_order_spec.rb - thread_order.gemspec homepage: https://github.com/JoshCheek/thread_order licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.1 signing_key: specification_version: 4 summary: Test helper for ordering threaded code test_files: - spec/run - spec/thread_order_spec.rb has_rdoc: thread-order-1.1.0/thread_order.gemspec0000644000175000017500000000124212537102161017453 0ustar boutilboutilrequire File.expand_path('../lib/thread_order/version', __FILE__) Gem::Specification.new do |s| s.name = 'thread_order' s.version = ThreadOrder::VERSION s.licenses = ['MIT'] s.summary = "Test helper for ordering threaded code" s.description = "Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2, rbx, jruby)." s.authors = ["Josh Cheek"] s.email = 'josh.cheek@gmail.com' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- spec/*`.split("\n") s.homepage = 'https://github.com/JoshCheek/thread_order' s.add_development_dependency 'rspec', '~> 3.0' end thread-order-1.1.0/spec/0000755000175000017500000000000012537102161014377 5ustar boutilboutilthread-order-1.1.0/spec/thread_order_spec.rb0000644000175000017500000001740612537102161020410 0ustar boutilboutilrequire 'thread_order' RSpec.describe ThreadOrder do let(:order) { described_class.new } after { order.apocalypse! } it 'allows thread behaviour to be declared and run by name' do seen = [] order.declare(:third) { seen << :third } order.declare(:first) { seen << :first; order.pass_to :second, :resume_on => :exit } order.declare(:second) { seen << :second; order.pass_to :third, :resume_on => :exit } expect(seen).to eq [] order.pass_to :first, :resume_on => :exit expect(seen).to eq [:first, :second, :third] end it 'sleeps the thread which passed' do main_thread = Thread.current order.declare(:thread) { :noop until main_thread.status == 'sleep' } order.pass_to :thread, :resume_on => :exit # passes if it doesn't lock up end context 'resume events' do def self.test_status(name, statuses, *args, &threadmaker) it "can resume the thread when the called thread enters #{name}", *args do thread = instance_eval(&threadmaker) statuses = Array statuses expect(statuses).to include thread.status end end test_status ':run', 'run' do order.declare(:t) { loop { 1 } } order.pass_to :t, :resume_on => :run end test_status ':sleep', 'sleep' do order.declare(:t) { sleep } order.pass_to :t, :resume_on => :sleep end # can't reproduce 'dead', but apparently JRuby 1.7.19 returned # this on CI https://travis-ci.org/rspec/rspec-core/jobs/51933739 test_status ':exit', [false, 'aborting', 'dead'] do order.declare(:t) { Thread.exit } order.pass_to :t, :resume_on => :exit end it 'passes the parent to the thread' do parent = nil order.declare(:t) { |p| parent = p } order.pass_to :t, :resume_on => :exit expect(parent).to eq Thread.current end it 'sleeps until woken if it does not provide a :resume_on key' do order.declare(:t) { |parent| order.enqueue { expect(parent.status).to eq 'sleep' parent.wakeup } } order.pass_to :t end it 'blows up if it is waiting on another thread to sleep and that thread exits instead' do expect { order.declare(:t1) { :exits_instead_of_sleeping } order.pass_to :t1, :resume_on => :sleep }.to raise_error ThreadOrder::CannotResume, /t1 exited/ end end describe 'error types' do it 'has a toplevel lib error: ThreadOrder::Error which is a RuntimeError' do expect(ThreadOrder::Error.superclass).to eq RuntimeError end specify 'all behavioural errors it raises inherit from ThreadOrder::Error' do expect(ThreadOrder::CannotResume.superclass).to eq ThreadOrder::Error end end describe 'errors in children' do specify 'are raised in the child' do order.declare(:err) { sleep } child = order.pass_to :err, :resume_on => :sleep begin child.raise RuntimeError.new('the roof') sleep rescue RuntimeError => e expect(e.message).to eq 'the roof' else raise 'expected an error' end end specify 'are raised in the parent' do expect { order.declare(:err) { raise Exception, "to the rules" } order.pass_to :err, :resume_on => :exit sleep }.to raise_error Exception, 'to the rules' end specify 'even if the parent is asleep' do order.declare(:err) { sleep } parent = Thread.current child = order.pass_to :err, :resume_on => :sleep expect { order.enqueue { expect(parent.status).to eq 'sleep' child.raise Exception.new 'to the rules' } sleep }.to raise_error Exception, 'to the rules' end end it 'knows which thread is running' do thread_names = [] order.declare(:a) { thread_names << order.current order.pass_to :b, :resume_on => :exit thread_names << order.current } order.declare(:b) { thread_names << order.current } order.pass_to :a, :resume_on => :exit expect(thread_names.map(&:to_s).sort).to eq ['a', 'a', 'b'] end it 'returns nil when asked for the current thread by one it did not define' do thread_names = [] order.declare(:a) { thread_names << order.current Thread.new { thread_names << order.current }.join } expect(order.current).to eq nil order.pass_to :a, :resume_on => :exit expect(thread_names).to eq [:a, nil] end it 'is implemented without depending on the stdlib' do loaded_filenames = $LOADED_FEATURES.map { |filepath| File.basename filepath } begin expect(loaded_filenames).to_not include 'monitor.rb' expect(loaded_filenames).to_not include 'thread.rb' expect(loaded_filenames).to_not include 'thread.bundle' rescue RSpec::Expectations::ExpectationNotMetError pending if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' # somehow this still gets loaded in some JRubies raise end end describe 'incorrect interface usage' do it 'raises ArgumentError when told to resume on an unknown status' do order.declare(:t) { } expect { order.pass_to :t, :resume_on => :bad_status }. to raise_error(ArgumentError, /bad_status/) end it 'raises an ArgumentError when you give it unknown keys (ie you spelled resume_on wrong)' do order.declare(:t) { } expect { order.pass_to :t, :bad_key => :t }. to raise_error(ArgumentError, /bad_key/) end end describe 'join_all' do it 'joins with all the child threads' do parent = Thread.current children = [] order.declare(:t1) do order.pass_to :t2, :resume_on => :run children << Thread.current end order.declare(:t2) do children << Thread.current end order.pass_to :t1, :resume_on => :run order.join_all statuses = children.map { |th| th.status } expect(statuses).to eq [false, false] # none are alive end end describe 'synchronization' do it 'allows any thread to enqueue work' do seen = [] order.declare :enqueueing do |parent| order.enqueue do order.enqueue { seen << 2 } order.enqueue { seen << 3 } order.enqueue { parent.wakeup } seen << 1 end end order.pass_to :enqueueing expect(seen).to eq [1, 2, 3] end it 'allows a thread to put itself to sleep until some condition is met' do i = 0 increment = lambda do i += 1 order.enqueue(&increment) end increment.call order.wait_until { i > 20_000 } # 100k is too slow on 1.8.7, but 10k is too fast on 2.2.0 expect(i).to be > 20_000 end end describe 'apocalypse!' do it 'kills threads that are still alive' do order.declare(:t) { sleep } child = order.pass_to :t, :resume_on => :sleep expect(child).to receive(:kill).and_call_original expect(child).to_not receive(:join) order.apocalypse! end it 'can be overridden to call a different method than kill' do # for some reason, the mock calling original join doesn't work order.declare(:t) { sleep } child = order.pass_to :t, :resume_on => :run expect(child).to_not receive(:kill) joiner = Thread.new { order.apocalypse! :join } Thread.pass until child.status == 'sleep' # can't use wait_until b/c that occurs within the worker, which is apocalypsizing child.wakeup joiner.join end it 'can call apocalypse! any number of times without harm' do order.declare(:t) { sleep } order.pass_to :t, :resume_on => :sleep 100.times { order.apocalypse! } end it 'does not enqueue events after the apocalypse' do order.apocalypse! thread = Thread.current order.enqueue { thread.raise "Should not happen" } end end end thread-order-1.1.0/spec/run0000755000175000017500000000301012537102161015123 0ustar boutilboutil#!/bin/bash # do this stuff in a tmp dir cd "$(dirname "$0")/.." project_root=`pwd` mkdir -p tmp cd tmp # this is basically a shitty version of `gem unpack` get_gem() { name="$1" url="$2" if test -d "$name" then echo "Skipping download of $name" return else echo "Downloading $name" fi mkdir "$name" && cd "$name" && curl -L "$url" > "$name".gem && tar -xf "$name".gem && gunzip data.tar.gz && tar -xf data.tar && cd .. } # download dependencies get_gem "rspec" "https://rubygems.org/downloads/rspec-3.2.0.gem" && get_gem "rspec-core" "https://rubygems.org/downloads/rspec-core-3.2.1.gem" && get_gem "rspec-support" "https://rubygems.org/downloads/rspec-support-3.2.2.gem" && get_gem "rspec-expectations" "https://rubygems.org/downloads/rspec-expectations-3.2.0.gem" && get_gem "rspec-mocks" "https://rubygems.org/downloads/rspec-mocks-3.2.1.gem" && get_gem "diff-lcs" "https://rubygems.org/downloads/diff-lcs-1.2.5.gem" || exit 1 # run specs cd "$project_root" export PATH="$project_root/tmp/rspec-core/exe:$PATH" opts=() opts+=(-I "$project_root/tmp/diff-lcs/lib") opts+=(-I "$project_root/tmp/rspec/lib") opts+=(-I "$project_root/tmp/rspec-core/lib") opts+=(-I "$project_root/tmp/rspec-expectations/lib") opts+=(-I "$project_root/tmp/rspec-mocks/lib") opts+=(-I "$project_root/tmp/rspec-support/lib") if `ruby -e "exit RUBY_VERSION != '1.8.7'"` then opts+=(--disable-gems) fi ruby "${opts[@]}" -S rspec --colour --fail-fast --format documentation thread-order-1.1.0/lib/0000755000175000017500000000000012537102161014213 5ustar boutilboutilthread-order-1.1.0/lib/thread_order/0000755000175000017500000000000012537102161016655 5ustar boutilboutilthread-order-1.1.0/lib/thread_order/version.rb0000644000175000017500000000005212537102161020664 0ustar boutilboutilclass ThreadOrder VERSION = '1.1.0' end thread-order-1.1.0/lib/thread_order/mutex.rb0000644000175000017500000000257712537102161020357 0ustar boutilboutilclass ThreadOrder Mutex = if defined? ::Mutex # On 1.9 and up, this is in core, so we just use the real one ::Mutex else # On 1.8.7, it's in the stdlib. # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment, # causing tests to pass where they should fail. # # So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56 # Some methods we don't need are deleted. # Anything I don't understand (there's quite a bit, actually) is left in. Class.new do def initialize @waiting = [] @locked = false; @waiting.taint self.taint end def lock while (Thread.critical = true; @locked) @waiting.push Thread.current Thread.stop end @locked = true Thread.critical = false self end def unlock return unless @locked Thread.critical = true @locked = false begin t = @waiting.shift t.wakeup if t rescue ThreadError retry end Thread.critical = false begin t.run if t rescue ThreadError end self end def synchronize lock begin yield ensure unlock end end end end end thread-order-1.1.0/lib/thread_order.rb0000644000175000017500000000560012537102161017203 0ustar boutilboutilrequire 'thread_order/mutex' class ThreadOrder Error = Class.new RuntimeError CannotResume = Class.new Error # Note that this must tbe initialized in a threadsafe environment # Otherwise, syncing may occur before the mutex is set def initialize @mutex = Mutex.new @bodies = {} @threads = [] @queue = [] # Queue is in stdlib, but half the purpose of this lib is to avoid such deps, so using an array in a Mutex @worker = Thread.new do Thread.current.abort_on_exception = true Thread.current[:thread_order_name] = :internal_worker loop { break if :shutdown == work() } end end def declare(name, &block) sync { @bodies[name] = block } end def current Thread.current[:thread_order_name] end def pass_to(name, options={}) child = nil parent = Thread.current resume_event = extract_resume_event!(options) enqueue do sync do @threads << Thread.new { child = Thread.current child[:thread_order_name] = name body = sync { @bodies.fetch(name) } wait_until { parent.stop? } :run == resume_event && parent.wakeup wake_on_sleep = lambda do child.status == 'sleep' ? parent.wakeup : child.status == nil ? :noop : child.status == false ? parent.raise(CannotResume.new "#{name} exited instead of sleeping") : enqueue(&wake_on_sleep) end :sleep == resume_event && enqueue(&wake_on_sleep) begin body.call parent rescue Exception => e enqueue { parent.raise e } raise ensure :exit == resume_event && enqueue { parent.wakeup } end } end end sleep child end def join_all sync { @threads }.each { |th| th.join } end def apocalypse!(thread_method=:kill) enqueue do @threads.each(&thread_method) @queue.clear :shutdown end @worker.join end def enqueue(&block) sync { @queue << block if @worker.alive? } end def wait_until(&condition) return if condition.call thread = Thread.current wake_when_true = lambda do if thread.stop? && condition.call thread.wakeup else enqueue(&wake_when_true) end end enqueue(&wake_when_true) sleep end private def sync(&block) @mutex.synchronize(&block) end def work task = sync { @queue.shift } task ||= lambda { Thread.pass } task.call end def extract_resume_event!(options) resume_on = options.delete :resume_on options.any? && raise(ArgumentError, "Unknown options: #{options.inspect}") resume_on && ![:run, :exit, :sleep, nil].include?(resume_on) and raise(ArgumentError, "Unknown status: #{resume_on.inspect}") resume_on || :none end end thread-order-1.1.0/Readme.md0000644000175000017500000000271412537102161015170 0ustar boutilboutil[![Build Status](https://travis-ci.org/JoshCheek/thread_order.svg)](https://travis-ci.org/JoshCheek/thread_order) ThreadOrder =========== A tool for testing threaded code. Its purpose is to enable reasoning about thread order. * Tested on 1.8.7 - 2.2, JRuby, Rbx * It has no external dependencies * It does not depend on the stdlib. Example ------- ```ruby # A somewhat contrived class we're going to test. class MyQueue attr_reader :array def initialize @array, @mutex = [], Mutex.new end def enqueue @mutex.synchronize { @array << yield } end end require 'rspec/autorun' require 'thread_order' RSpec.describe MyQueue do let(:queue) { described_class.new } let(:order) { ThreadOrder.new } after { order.apocalypse! } # ensure everything gets cleaned up (technically redundant for our one example, but it's a good practice) it 'is threadsafe on enqueue' do # will execute in a thread, can be invoked by name order.declare :concurrent_enqueue do queue.enqueue { :concurrent } end # this enqueue will block until the mutex puts the other one to sleep queue.enqueue do order.pass_to :concurrent_enqueue, resume_on: :sleep :main end order.join_all # concurrent_enqueue may still be asleep expect(queue.array).to eq [:main, :concurrent] end end # >> MyQueue # >> is threadsafe on enqueue # >> # >> Finished in 0.00131 seconds (files took 0.08687 seconds to load) # >> 1 example, 0 failures ``` thread-order-1.1.0/License.txt0000644000175000017500000000206112537102161015567 0ustar boutilboutil(The MIT License) Copyright (c) 2015 Josh Cheek 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 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. thread-order-1.1.0/Gemfile0000644000175000017500000000004612537102161014740 0ustar boutilboutilsource 'https://rubygems.org' gemspec thread-order-1.1.0/.travis.yml0000644000175000017500000000037312537102161015561 0ustar boutilboutillanguage: ruby script: spec/run rvm: - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 - 2.1 - 2.2 - ruby-head - ree - jruby-18mode - jruby - jruby-head - rbx matrix: include: - rvm: jruby env: JRUBY_OPTS='--2.0' fast_finish: true thread-order-1.1.0/.gitignore0000644000175000017500000000003512537102161015433 0ustar boutilboutil*.gem Gemfile.lock tmp/ .rbx