flores-0.0.8/0000755000175000017500000000000014537405540014116 5ustar thegodtunethegodtuneflores-0.0.8/.gitignore0000644000175000017500000000001214537405540016077 0ustar thegodtunethegodtunecoverage/ flores-0.0.8/examples/0000755000175000017500000000000014537405540015734 5ustar thegodtunethegodtuneflores-0.0.8/examples/socket_stress_spec.rb0000644000175000017500000000336714537405540022177 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/rspec" require "flores/random" require "socket" RSpec.configure do |config| Flores::RSpec.configure(config) Kernel.srand(config.seed) # Demonstrate the wonderful Analyze formatter config.add_formatter("Flores::RSpec::Formatters::Analyze") end describe TCPServer do analyze_results subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) } let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") } after do socket.close unless socket.closed? end context "on a random port" do let(:port) { Flores::Random.integer(-100_000..100_000) } stress_it "should bind successfully", [:port] do socket.bind(sockaddr) expect(socket.local_address.ip_port).to(be == port) end end context "on privileged ports" do let(:port) { Flores::Random.integer(1..1023) } stress_it "should raise Errno::EACCESS" do expect { socket.bind(sockaddr) }.to(raise_error(Errno::EACCES)) end end context "on unprivileged ports" do let(:port) { Flores::Random.integer(1025..65535) } stress_it "should bind on a port" do # EADDRINUSE is expected since we are picking ports at random # Let's ignore this specific exception allow(socket).to(receive(:bind).and_wrap_original do |original, *args| begin original.call(*args) rescue Errno::EADDRINUSE # rubocop:disable Lint/HandleExceptions # Ignore end end) expect { socket.bind(sockaddr) }.to_not(raise_error) end end context "on port 0" do let(:port) { 0 } stress_it "should bind successfully" do expect { socket.bind(sockaddr) }.to_not(raise_error) end end end flores-0.0.8/examples/analyze_number.rb0000644000175000017500000000110614537405540021272 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/rspec" require "flores/random" RSpec.configure do |config| Flores::RSpec.configure(config) Kernel.srand config.seed # Demonstrate the wonderful Analyze formatter config.add_formatter("Flores::RSpec::Formatters::Analyze") end describe "a random number" do analyze_results context "between 0 and 200 inclusive" do let(:number) { Flores::Random.number(0..200) } stress_it "should be less than 100" do expect(number).to(be < 100) end end end flores-0.0.8/examples/socket_acceptance_spec.rb0000644000175000017500000000362714537405540022741 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/rspec" require "flores/random" require "socket" RSpec.configure do |config| Kernel.srand config.seed Flores::RSpec.configure(config) # Demonstrate the wonderful Analyze formatter config.add_formatter("Flores::RSpec::Formatters::Analyze") end # A factory for encapsulating behavior of a tcp server and client for the # purposes of testing. # # This is probably not really a "factory" in a purist-sense, but whatever. class TCPIntegrationTestFactory def initialize(port) @listener = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) @client = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) @port = port end def teardown @listener.close unless @listener.closed? @client.close unless @listener.closed? end def sockaddr Socket.sockaddr_in(@port, "127.0.0.1") end def setup @listener.bind(sockaddr) @listener.listen(5) end def send_and_receive(text) @client.connect(sockaddr) server, _ = @listener.accept @client.syswrite(text) @client.close data = server.read data.force_encoding(Encoding.default_external).encoding data ensure @client.close unless @client.closed? server.close unless server.nil? || server.closed? end end describe "TCPServer+TCPSocket" do analyze_results let(:port) { Flores::Random.integer(1024..65535) } let(:text) { Flores::Random.text(1..2000) } subject { TCPIntegrationTestFactory.new(port) } before do begin subject.setup rescue Errno::EADDRINUSE skip "Port #{port} was in use. Skipping!" end end stress_it "should send data correctly", [:port, :text] do begin received = subject.send_and_receive(text) expect(received.encoding).to(be == text.encoding) expect(received).to(be == text) ensure subject.teardown end end end flores-0.0.8/lib/0000755000175000017500000000000014537405540014664 5ustar thegodtunethegodtuneflores-0.0.8/lib/flores/0000755000175000017500000000000014537405540016156 5ustar thegodtunethegodtuneflores-0.0.8/lib/flores/rspec.rb0000644000175000017500000000157214537405540017624 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # # :nodoc: require "flores/namespace" # The root of the rspec helpers the Flores library provides module Flores::RSpec DEFAULT_ITERATIONS = 1..1000 # Sets up rspec with the Flores RSpec helpers. Usage looks like this: # # RSpec.configure do |config| # Flores::RSpec.configure(config) # end def self.configure(rspec_configuration) require "flores/rspec/stress" require "flores/rspec/analyze" rspec_configuration.extend(Flores::RSpec::Stress) rspec_configuration.extend(Flores::RSpec::Analyze) end # def self.configure def self.iterations return @iterations if @iterations if ENV["ITERATIONS"] @iterations = 0..ENV["ITERATIONS"].to_i else @iterations = DEFAULT_ITERATIONS end @iterations end end # def Flores::RSpec flores-0.0.8/lib/flores/rspec/0000755000175000017500000000000014537405540017272 5ustar thegodtunethegodtuneflores-0.0.8/lib/flores/rspec/analyze.rb0000644000175000017500000001266414537405540021273 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/namespace" require "flores/rspec" # RSpec helpers for stress testing examples # # Setting it up in rspec: # # RSpec.configure do |c| # c.extend RSpec::StressIt # end # # TODO(sissel): Show an example of stress_it and analyze_it module Flores::RSpec::Analyze # Save state after each example so it can be used in analysis after specs are completed. # # If you use this, you'll want to set your RSpec formatter to # Flores::RSpec::Formatter::Analyze # # Let's show an example that fails sometimes. # # describe "Addition of two numbers" do # context "positive numbers" do # analyze_results # let(:a) { Flores::Random.number(1..1000) } # # # Here we make negative numbers possible to cause failure in our test. # let(:b) { Flores::Random.number(-200..1000) } # subject { a + b } # # stress_it "should be positive" do # expect(subject).to(be > 0) # end # end # end # # And running it: # # % rspec -f Flores::RSpec::Formatter::Analyze # Addition of two numbers positive numbers should be positive # 98.20% tests successful of 3675 tests # Failure analysis: # 1.80% -> [66] RSpec::Expectations::ExpectationNotMetError # Sample exception for {:a=>126.21705882478048, :b=>-139.54814492675024, :subject=>-13.33108610196976} # expected: > 0 # got: -13.33108610196976 # Samples causing RSpec::Expectations::ExpectationNotMetError: # {:a=>90.67298249206425, :b=>-136.6237821353908, :subject=>-45.95079964332655} # {:a=>20.35865155878871, :b=>-39.592417377658876, :subject=>-19.233765818870165} # {:a=>158.07905166101787, :b=>-177.5864470909581, :subject=>-19.50739542994023} # {:a=>31.80445518715138, :b=>-188.51942190504894, :subject=>-156.71496671789757} # {:a=>116.1479954937354, :b=>-146.18477887927958, :subject=>-30.036783385544183} def analyze_results # TODO(sissel): Would be lovely to figure out how to inject an 'after' for # all examples if we are using the Analyze formatter. # Then this method could be implied by using the right formatter, or something. after do |example| example.metadata[:values] = __memoized.clone end end # A formatter to show analysis of an `analyze_it` example. class Analysis < StandardError def initialize(results) @results = results end # def initialize def total @results.reduce(0) { |m, (_, v)| m + v.length } end # def total def success_count if @results.include?(:passed) @results[:passed].length else 0 end end # def success_count def success_and_pending_count count = 0 [:passed, :pending].each do |group| count += @results[group].length end count end # def success_count def percent(count) return (count + 0.0) / total end # def percent def percent_s(count) return format("%.2f%%", percent(count) * 100) end # def percent_s def to_s # rubocop:disable Metrics/AbcSize # This method is crazy complex for a formatter. Should refactor this significantly. report = [] if @results[:pending].any? # We have pending examples, put a clear message. report << "#{percent_s(success_and_pending_count)} (of #{total} total) tests are successful or pending" else report << "#{percent_s(success_count)} (of #{total} total) tests are successful" end report += failure_summary if success_and_pending_count < total report.join("\n") end # def to_s # TODO(sissel): All these report/summary/to_s things are an indication that the # report formatting belongs in a separate class. def failure_summary report = ["Failure analysis:"] report += @results.sort_by { |_, v| -v.length }.collect do |group, instances| next if group == :passed next if group == :pending error_report(group, instances) end.reject(&:nil?).flatten report end # def failure_summary def error_report(error, instances) report = error_summary(error, instances) report += error_sample_states(error, instances) if instances.size > 1 report end # def error_report def error_summary(error, instances) sample = instances.sample(1) [ " #{percent_s(instances.length)} -> [#{instances.length}] #{error}", " Sample failure", " Inputs:", *render_values(sample.first[0]).map { |x| " #{x}" }, " Exception:", sample.first[1].to_s.gsub(/^/, " ") ] end # def error_summary def render_values(values) # values should be an RSpec::Core::MemoizedHelpers::ThreadsafeMemoized lets = values.instance_eval { @memoized } return [""] if lets.nil? lets.sort_by { |k,v| v.to_s.size }.map do |k,v| if v.to_s.size > 50 v = v.to_s[0, 50] + "..." end "#{k}=#{v}" end end def error_sample_states(error, instances) [ " Samples causing #{error}:", *instances.sample(5).collect { |state, _exception| " #{state}" } ] end # def error_sample_states end # class Analysis end # Flores::RSpec::Analyze flores-0.0.8/lib/flores/rspec/stress.rb0000644000175000017500000000632114537405540021144 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/namespace" require "flores/rspec" require "flores/random" # This module adds helpers useful in doing stress testing within rspec. # # The number of iterations in a stress test is random. # # By way of example, let's have a silly test for adding two positive numbers and expecting # the result to not be negative: # # describe "Addition" do # context "of two positive numbers" do # let(:a) { Flores::Random.number(1..10000) } # let(:b) { Flores::Random.number(1..10000) } # subject { a + b } # # # Note the use of 'stress_it' here! # stress_it "should be greater than zero" do # expect(subject).to(be > 0) # end # end # end # # Running this: # # % rspec # # # Finished in 0.45412 seconds (files took 0.32963 seconds to load) # 4795 examples, 0 failures # # In this way, instead of testing 1 fixed case or 1 randomized case, we test # *many* cases in one rspec run. module Flores::RSpec::Stress # Wraps `it` and runs the block many times. Each run has will clear the `let` cache. # # The implementation of this is roughly that the given block will be run N times within an `it`: # # stress_it_internal "my test" do # expect(...) # end # # is roughly equivalent to # # it "my test" do # 1000.times do # expect(...) # __memoized.clear # end # end # # The intent of this is to allow randomized testing for fuzzing and stress testing # of APIs to help find edge cases and weird behavior. # # The default number of iterations is randomly selected between 1 and 1000 inclusive def stress_it_internal(name, options = {}, &block) stress__iterations = Flores::Random.iterations(options.delete(:stress_iterations) || Flores::RSpec::DEFAULT_ITERATIONS) it(name, options) do # Run the block of an example many times stress__iterations.each do # Run the block within 'it' scope instance_eval(&block) # clear the internal rspec `let` cache this lets us run a test # repeatedly with fresh `let` evaluations. # Reference: https://github.com/rspec/rspec-core/blob/5fc29a15b9af9dc1c9815e278caca869c4769767/lib/rspec/core/memoized_helpers.rb#L124-L127 __memoized.clear end end # it ... end # def stress_it_internal # Generate a random number of copies of a given example. # The idea is to take 1 `it` and run it N times to help tease out failures. # Of course, the teasing requires you have randomized `let` usage, for example: # # let(:number) { Flores::Random.number(0..200) } # it "should be less than 100" do # expect(number).to(be < 100) # end # # This creates N (random) copies of your spec example. Using `stress_it` is # preferred instead of `stress_it_internal` because this method will cause # before, after, and around clauses to be invoked correctly. def stress_it(name, *args, &block) Flores::Random.iterations(Flores::RSpec.iterations).each do it(name, *args, &block) end # each end # def stress_it end # Flores::RSpec::Stress flores-0.0.8/lib/flores/rspec/formatters/0000755000175000017500000000000014537405540021460 5ustar thegodtunethegodtuneflores-0.0.8/lib/flores/rspec/formatters/analyze.rb0000644000175000017500000000512414537405540023452 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/namespace" require "rspec/core/formatters/base_text_formatter" Flores::RSpec::Formatters::Analyze = Class.new(RSpec::Core::Formatters::BaseTextFormatter) do RSpec::Core::Formatters.register self, :dump_failures, :dump_summary, :start, :example_passed, :example_failed, :example_pending SPINNER = %w(▘ ▝ ▗ ▖) def example_passed(_event) increment(:pass) end def example_failed(_event) increment(:failed) end def example_pending(_event) increment(:pending) end def increment(status) return unless output.tty? now = Time.new if status == :failed output.write("F") elsif status == :pending output.write("P") end update_status if now - @last_update > 0.200 end def update_status glyph = SPINNER[@count] output.write("#{glyph} ") @last_update = Time.new @count += 1 @count = 0 if @count >= SPINNER.size end def start(event) @last_update = Time.now @total = event.count @count = 0 end def dump_summary(event) output.write("\r") if output.tty? # The event is an RSpec::Core::Notifications::SummaryNotification # Let's mimic the BaseTextFormatter but without the failing test report output.puts "Finished in #{event.formatted_duration}" output.puts "#{event.colorized_totals_line}" end def failures?(examples) return examples.select { |e| e.metadata[:execution_result].status == :failed }.any? end def dump_failures(event) return unless failures?(event.examples) group = event.examples.each_with_object(Hash.new { |h, k| h[k] = [] }) do |e, m| m[e.metadata[:full_description]] << e m end group.each { |description, examples| dump_example_summary(description, examples) } end def dump_example_summary(description, examples) output.puts description analysis = Flores::RSpec::Analyze::Analysis.new(group_by_result(examples)) output.puts(analysis.to_s.gsub(/^/, " ")) end def group_by_result(examples) # rubocop:disable Metrics/AbcSize examples.each_with_object(Hash.new { |h, k| h[k] = [] }) do |example, results| status = example.metadata[:execution_result].status case status when :passed, :pending results[status] << [example.metadata[:values], nil] else exception = example.metadata[:execution_result].exception results[exception.class] << [example.metadata[:values], exception] end results end end def method_missing(m, *args) p m => args end end flores-0.0.8/lib/flores/pki/0000755000175000017500000000000014537405540016741 5ustar thegodtunethegodtuneflores-0.0.8/lib/flores/pki/csr.rb0000644000175000017500000002036214537405540020060 0ustar thegodtunethegodtunerequire "flores/namespace" module Flores::PKI # A certificate signing request. # # From here, you can configure a certificate to be created based on your # desired configuration. # # Example making a root CA: # # key = OpenSSL::PKey::RSA.generate(4096, 65537) # csr = Flores::PKI::CertificateSigningRequest.new # csr.subject = "OU=Fancy Pants Inc." # certificate = csr.create_root(key) # # Example making an intermediate CA: # # root_key = OpenSSL::PKey::RSA.generate(4096, 65537) # root_csr = Flores::PKI::CertificateSigningRequest.new # root_csr.subject = "OU=Fancy Pants Inc." # root_csr.public_key = root_key.public # root_certificate = csr.create_root(root_key) # # intermediate_key = OpenSSL::PKey::RSA.generate(4096, 65537) # intermediate_csr = Flores::PKI::CertificateSigningRequest.new # intermediate_csr.public_key = intermediate_key.public # intermediate_csr.subject = "OU=Fancy Pants Inc. Intermediate 1" # intermediate_certificate = csr.create_intermediate(root_certificate, root_key) class CertificateSigningRequest # raised when an invalid signing configuration is given class InvalidRequest < StandardError; end # raised when invalid data is present in a certificate request class InvalidData < StandardError; end # raised when an invalid subject (format, or whatever) is given in a certificate request class InvalidSubject < InvalidData; end # raised when an invalid time value is given for a certificate request class InvalidTime < InvalidData; end def initialize self.serial = Flores::PKI.random_serial self.digest_method = default_digest_method end private def validate_subject(value) OpenSSL::X509::Name.parse(value) rescue OpenSSL::X509::NameError => e raise InvalidSubject, "Invalid subject '#{value}'. (#{e})" rescue TypeError => e # Bug(?) in MRI 2.1.6(?) raise InvalidSubject, "Invalid subject '#{value}'. (#{e})" end def subject=(value) @subject = validate_subject(value) end attr_reader :subject def subject_alternates=(values) @subject_alternates = values end attr_reader :subject_alternates def public_key=(value) @public_key = validate_public_key(value) end def validate_public_key(value) raise InvalidData, "public key must be a OpenSSL::PKey::PKey" unless value.is_a? OpenSSL::PKey::PKey value end attr_reader :public_key def start_time=(value) @start_time = validate_time(value) end attr_reader :start_time def expire_time=(value) @expire_time = validate_time(value) end attr_reader :expire_time def validate_time(value) raise InvalidTime, "#{value.inspect} (class #{value.class.name})" unless value.is_a?(Time) value end def certificate return @certificate if @certificate @certificate = OpenSSL::X509::Certificate.new # RFC5280 # > 4.1.2.1. Version # > version MUST be 3 (value is 2). # # Version value of '2' means a v3 certificate. @certificate.version = 2 @certificate.subject = subject @certificate.not_before = start_time @certificate.not_after = expire_time @certificate.public_key = public_key @certificate end def default_digest_method OpenSSL::Digest::SHA256.new end def self_signed? @signing_certificate.nil? end def validate! if self_signed? if @signing_key.nil? raise InvalidRequest, "No signing_key given. Cannot sign key." end elsif @signing_certificate.nil? && @signing_key raise InvalidRequest, "signing_key given, but no signing_certificate is set" elsif @signing_certificate && @signing_key.nil? raise InvalidRequest, "signing_certificate given, but no signing_key is set" end end def create validate! extensions = OpenSSL::X509::ExtensionFactory.new extensions.subject_certificate = certificate extensions.issuer_certificate = self_signed? ? certificate : signing_certificate certificate.issuer = extensions.issuer_certificate.subject certificate.add_extension(extensions.create_extension("subjectKeyIdentifier", "hash", false)) # RFC 5280 4.2.1.1. Authority Key Identifier # This is "who signed this key" certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always", false)) #certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always", false)) if want_signature_ability? # Create a CA. certificate.add_extension(extensions.create_extension("basicConstraints", "CA:TRUE", true)) # Rough googling seems to indicate at least keyCertSign is required for CA and intermediate certs. certificate.add_extension(extensions.create_extension("keyUsage", "keyCertSign, cRLSign, digitalSignature", true)) else # Create a client+server certificate # # It feels weird to create a certificate that's valid as both server and client, but a brief inspection of major # web properties (apple.com, google.com, yahoo.com, github.com, fastly.com, mozilla.com, amazon.com) reveals that # major web properties have certificates with both clientAuth and serverAuth extended key usages. Further, # these major server certificates all have digitalSignature and keyEncipherment for key usage. # # Here's the command I used to check this: # echo mozilla.com apple.com github.com google.com yahoo.com fastly.com elastic.co amazon.com \ # | xargs -n1 sh -c 'openssl s_client -connect $1:443 \ # | sed -ne "/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p" \ # | openssl x509 -text -noout | sed -ne "/X509v3 extensions/,/Signature Algorithm/p" | sed -e "s/^/$1 /"' - \ # | grep -A2 'Key Usage' certificate.add_extension(extensions.create_extension("keyUsage", "digitalSignature, keyEncipherment", true)) certificate.add_extension(extensions.create_extension("extendedKeyUsage", "clientAuth, serverAuth", false)) end if @subject_alternates certificate.add_extension(extensions.create_extension("subjectAltName", @subject_alternates.join(","))) end certificate.serial = OpenSSL::BN.new(serial) certificate.sign(signing_key, digest_method) certificate end # Set the certificate which is going to be signing this request. def signing_certificate=(certificate) raise InvalidData, "signing_certificate must be an OpenSSL::X509::Certificate" unless certificate.is_a?(OpenSSL::X509::Certificate) @signing_certificate = certificate end attr_reader :signing_certificate attr_reader :signing_key def signing_key=(private_key) raise InvalidData, "signing_key must be an OpenSSL::PKey::PKey (or a subclass)" unless private_key.is_a?(OpenSSL::PKey::PKey) @signing_key = private_key end def want_signature_ability=(value) raise InvalidData, "want_signature_ability must be a boolean" unless value == true || value == false @want_signature_ability = value end def want_signature_ability? @want_signature_ability == true end attr_reader :digest_method def digest_method=(value) raise InvalidData, "digest_method must be a OpenSSL::Digest (or a subclass)" unless value.is_a?(OpenSSL::Digest) @digest_method = value end attr_reader :serial def serial=(value) begin Integer(value) rescue raise InvalidData, "Invalid serial value. Must be a number (or a String containing only nubers)" end @serial = value end public(:serial, :serial=) public(:subject, :subject=) public(:subject_alternates, :subject_alternates=) public(:public_key, :public_key=) public(:start_time, :start_time=) public(:expire_time, :expire_time=) public(:digest_method, :digest_method=) public(:want_signature_ability?, :want_signature_ability=) public(:signing_key, :signing_key=) public(:signing_certificate, :signing_certificate=) public(:create) end # class CertificateSigningRequest end flores-0.0.8/lib/flores/pki.rb0000644000175000017500000000360514537405540017272 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/namespace" require "flores/random" require "flores/pki/csr" require "English" require "openssl" module Flores::PKI GENERATE_DEFAULT_KEY_SIZE = 1024 GENERATE_DEFAULT_EXPONENT = 65537 GENERATE_DEFAULT_DURATION_RANGE = 1..86400 class << self # Generate a random serial number for a certificate. def random_serial # RFC5280 (X509) says: # > 4.1.2.2. Serial Number # > Certificate users MUST be able to handle serialNumber values up to 20 octets Flores::Random.integer(1..9).to_s + Flores::Random.iterations(0..19).collect { Flores::Random.integer(0..9) }.join end # Generate a valid certificate with sane random values. # # By default this method use `CN=localhost` as the default subject and a 1024 bits encryption # key for the certificate, you can override the defaults by specifying a subject and the # key size in the options hash. # # Example: # # Flores::PKI.generate("CN=localhost", { :key_size => 2048 } # # @params subject [String] Certificate subject # @params opts [Hash] Options # @return [OpenSSL::X509::Certificate, OpenSSL::Pkey::RSA] def generate(subject = "CN=localhost", opts = {}) key_size = opts.fetch(:key_size, GENERATE_DEFAULT_KEY_SIZE) key = OpenSSL::PKey::RSA.generate(key_size, GENERATE_DEFAULT_EXPONENT) certificate_duration = Flores::Random.number(GENERATE_DEFAULT_DURATION_RANGE) csr = Flores::PKI::CertificateSigningRequest.new csr.subject = subject csr.public_key = key.public_key csr.start_time = Time.now csr.expire_time = csr.start_time + certificate_duration csr.signing_key = key csr.want_signature_ability = true certificate = csr.create return [certificate, key] end end end # Flores::PKI flores-0.0.8/lib/flores/random.rb0000644000175000017500000001245114537405540017766 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "flores/namespace" autoload :Socket, "socket" # A collection of methods intended for use in randomized testing. module Flores::Random # A selection of UTF-8 characters # # I'd love to generate this, but I don't yet know enough about how unicode # blocks are allocated to do that. For now, hardcode a set of possible # characters. CHARACTERS = [ # Basic Latin *(32..126).map(&:chr).map { |c| c.force_encoding(Encoding.default_external) }, # hand-selected CJK Unified Ideographs Extension A "㐤", "㐨", "㐻", "㑐", # hand-selected Hebrew "א", "ב", "ג", "ד", "ה", # hand-selected Cyrillic "Є", "Б", "Р", "н", "я" ] # Generates text with random characters of a given length (or within a length range) # # * The length can be a number or a range `x..y`. If a range, it must be ascending (x < y) # * Negative lengths are not permitted and will raise an ArgumentError # # @param length [Fixnum or Range] the length of text to generate # @return [String] the generated text def self.text(length) return text_range(length) if length.is_a?(Range) raise ArgumentError, "A negative length is not permitted, I received #{length}" if length < 0 length.times.collect { character }.join end # def text # Generate text with random characters of a length within the given range. # # @param range [Range] the range of length to generate, inclusive # @return [String] the generated text def self.text_range(range) raise ArgumentError, "Requires ascending range, you gave #{range}." if range.end < range.begin raise ArgumentError, "A negative range values are not permitted, I received range #{range}" if range.begin < 0 text(integer(range)) end # Generates a random character (A string of length 1) # # @return [String] def self.character return CHARACTERS[integer(0...CHARACTERS.length)] end # def character # Return a random integer value within a given range. # # @param range [Range] def self.integer(range) raise ArgumentError, "Range not given, got #{range.class}: #{range.inspect}" if !range.is_a?(Range) rand(range) end # def integer # Return a random number within a given range. # # @param range [Range] def self.number(range) raise ArgumentError, "Range not given, got #{range.class}: #{range.inspect}" if !range.is_a?(Range) # Ruby 1.9.3 and below do not have Enumerable#size, so we have to compute the size of the range # ourselves. rand * (range.end - range.begin) + range.begin end # def number # Run a block a random number of times. # # @param range [Fixnum of Range] same meaning as #integer(range) def self.iterations(range, &block) range = 0..range if range.is_a?(Numeric) if block_given? integer(range).times(&block) nil else integer(range).times end end # def iterations # Return a random element from an array def self.item(array) array[integer(0...array.size)] end # Return a random IPv4 address as a string def self.ipv4 # TODO(sissel): Support CIDR range restriction? # TODO(sissel): Support netmask restriction? [integer(0..IPV4_MAX)].pack("N").unpack("C4").join(".") end # Return a random IPv6 address as a string # # The address may be in abbreviated form (ABCD::01EF):w def self.ipv6 # TODO(sissel): Support CIDR range restriction? # TODO(sissel): Support netmask restriction? length = integer(2..8) if length == 8 # Full address; nothing to abbreviate ipv6_pack(length) else abbreviation = ipv6_abbreviation(length) if length == 2 first = 1 second = 1 else first = integer(2...length) second = length - first end ipv6_pack(first) + abbreviation + ipv6_pack(second) end end # Get a TCP socket bound and listening on a random port. # # You are responsible for closing the socket. # # Returns [socket, address, port] def self.tcp_listener(host = "127.0.0.1") socket_listener(Socket::SOCK_STREAM, host) end # Get a UDP socket bound and listening on a random port. # # You are responsible for closing the socket. # # Returns [socket, address, port] def self.udp_listener(host = "127.0.0.1") socket_listener(Socket::SOCK_DGRAM, host) end private IPV4_MAX = 1 << 32 IPV6_SEGMENT = 1 << 16 def self.ipv6_pack(length) length.times.collect { integer(0...IPV6_SEGMENT).to_s(16) }.join(":") end def self.ipv6_abbreviation(length) abbreviate = (integer(0..1) == 0) if abbreviate "::" else ":" + (8 - length).times.collect { "0" }.join(":") + ":" end end LISTEN_BACKLOG = 5 def self.socket_listener(type, host) socket = server_socket_class.new(Socket::AF_INET, type) socket.bind(Socket.pack_sockaddr_in(0, host)) if type == Socket::SOCK_STREAM || type == Socket::SOCK_SEQPACKET socket.listen(LISTEN_BACKLOG) end port = socket.local_address.ip_port address = socket.local_address.ip_address [socket, address, port] end def self.server_socket_class if RUBY_ENGINE == 'jruby' # https://github.com/jruby/jruby/wiki/ServerSocket ServerSocket else Socket end end end # module Flores::Random flores-0.0.8/lib/flores/namespace.rb0000644000175000017500000000047014537405540020440 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # # :nodoc: module Flores # rubocop:disable Style/ClassAndModuleChildren module RSpec # rubocop:disable Style/ClassAndModuleChildren module Formatters # rubocop:disable Style/ClassAndModuleChildren end end end flores-0.0.8/LICENSE.txt0000644000175000017500000002170514537405540015746 0ustar thegodtunethegodtuneApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS flores-0.0.8/Makefile0000644000175000017500000000142714537405540015562 0ustar thegodtunethegodtuneGEMSPEC=$(shell ls *.gemspec) VERSION=$(shell awk -F\" '/spec.version/ { print $$2 }' $(GEMSPEC)) NAME=$(shell awk -F\" '/spec.name/ { print $$2 }' $(GEMSPEC)) GEM=$(NAME)-$(VERSION).gem .PHONY: package package: | $(GEM) .PHONY: gem gem: $(GEM) $(GEM): gem build $(GEMSPEC) .PHONY: test-package test-package: $(GEM) # Sometimes 'gem build' makes a faulty gem. gem unpack $(GEM) rm -rf $(NAME)-$(VERSION)/ .PHONY: publish publish: test-package gem push $(GEM) .PHONY: install install: $(GEM) gem install $(GEM) .PHONY: clean clean: -rm -rf .yardoc $(GEM) $(NAME)-$(VERSION)/ .PHONY: rubocop rubocop: rubocop -D .PHONY: test test: rubocop rspec -f Flores::RSpec::Formatters::Analyze .PHONY: test test-fast: rubocop ITERATIONS=10 rspec -f Flores::RSpec::Formatters::Analyze flores-0.0.8/flores.gemspec0000644000175000017500000000110314537405540016750 0ustar thegodtunethegodtuneGem::Specification.new do |spec| files = %x(git ls-files).split("\n") spec.name = "flores" spec.version = "0.0.8" spec.summary = "Fuzz, randomize, and stress your tests" spec.description = <<-DESCRIPTION Add fuzzing, randomization, and stress to your tests. This library is an exploration to build the tools to let you write tests that find bugs. In memory of Carlo Flores. DESCRIPTION spec.license = "Apache-2.0" spec.files = files spec.require_paths << "lib" spec.authors = ["Jordan Sissel"] spec.email = ["jls@semicomplete.com"] end flores-0.0.8/spec/0000755000175000017500000000000014537405540015050 5ustar thegodtunethegodtuneflores-0.0.8/spec/flores/0000755000175000017500000000000014537405540016342 5ustar thegodtunethegodtuneflores-0.0.8/spec/flores/random_spec.rb0000644000175000017500000001421314537405540021162 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "spec_init" shared_examples_for String do stress_it "should be a String" do expect(subject).to(be_a(String)) end stress_it "have expected encoding" do expect(subject.encoding).to(be == Encoding.default_external) end stress_it "have valid encoding" do expect(subject).to(be_valid_encoding) end end shared_examples_for "network address" do before { require "socket" } stress_it "should be a valid ipv6 address according to Socket.pack_sockaddr_in" do expect { Socket.pack_sockaddr_in(0, subject) }.not_to(raise_error) end end shared_examples_for Socket do stress_it "should be a Socket" do expect(socket).to(be_a(Socket)) end end describe Flores::Random do analyze_results describe "#text" do context "with no arguments" do stress_it "should raise ArgumentError" do expect { subject.text }.to(raise_error(ArgumentError)) end end context "with 1 length argument" do subject { described_class.text(length) } context "that is positive" do let(:length) { Flores::Random.integer(1..1000) } it_behaves_like String, [:length] stress_it "has correct length" do expect(subject.length).to(eq(length)) end stress_it "has correct encoding" do expect(subject.encoding).to(be == Encoding.default_external) end end context "that is negative" do let(:length) { -1 * Flores::Random.integer(1..1000) } stress_it "should raise ArgumentError" do expect { subject }.to(raise_error(ArgumentError)) end end end context "with 1 range argument" do let(:start) { Flores::Random.integer(2..1000) } let(:length) { Flores::Random.integer(1..1000) } subject { described_class.text(range) } context "that is ascending" do let(:range) { start..(start + length) } it_behaves_like String, [:range] stress_it "should give a string within that length range" do expect(subject).to(be_a(String)) expect(range).to(include(subject.length)) end end context "that is descending" do let(:range) { start..(start - length) } stress_it "should raise ArgumentError" do expect { subject }.to(raise_error(ArgumentError)) end end end end describe "#character" do subject { described_class.character } it_behaves_like String, [:subject] stress_it "has length == 1" do expect(subject.length).to(be == 1) end end shared_examples_for Numeric do |type| let(:start) { Flores::Random.integer(-100_000..100_000) } let(:length) { Flores::Random.integer(1..100_000) } let(:range) { start..(start + length) } stress_it "should be a #{type}" do expect(subject).to(be_a(type)) end stress_it "should be within the bounds of the given range" do expect(range).to(include(subject)) end end describe "#integer" do it_behaves_like Numeric, Fixnum do subject { Flores::Random.integer(range) } end end describe "#number" do it_behaves_like Numeric, Float do subject { Flores::Random.number(range) } end end describe "#iterations" do let(:start) { Flores::Random.integer(1..1000) } let(:length) { Flores::Random.integer(1..1000) } let(:range) { start..(start + length) } subject { Flores::Random.iterations(range) } stress_it "should return an Enumerable" do expect(subject).to(be_a(Enumerable)) end stress_it "should have a size within the expected range" do # Ruby 2.0 added Enumerable#size, so we can't use it here. # Meaning `123.times.size` doesn't work. So for this test, # we use small ranges because Enumerable#count actually # counts (via iteration) and is slow on large numbers. expect(range).to(include(subject.count)) end context "{ ... }" do stress_it "should invoke a given block for each iteration" do count = 0 Flores::Random.iterations(range) do count += 1 end expect(count).to(be > 0) expect(range).to(include(count)) end end end describe "#items" do let(:start) { Flores::Random.integer(1..1000) } let(:length) { Flores::Random.integer(1..1000) } let(:range) { start..(start + length) } let(:items) { Flores::Random.iterations(range).collect { Flores::Random.number(1..1000) } } subject { Flores::Random.item(items) } stress_it "should choose a random item from the list" do expect(items).to(include(subject)) end context "with a list of numbers" do stress_it "should be return a number" do expect(subject).to(be_a(Numeric)) end end end describe "#ipv6" do subject { Flores::Random.ipv6 } it_behaves_like "network address" end describe "#ipv4" do subject { Flores::Random.ipv4 } it_behaves_like "network address" end describe "networking" do let(:socket) { subject[0] } let(:host) { subject[1] } let(:port) { subject[2] } after do socket.close end describe "#udp_listener" do let(:text) { Flores::Random.text(1..100) } subject { Flores::Random.udp_listener } it_behaves_like Socket context "#recvfrom" do let(:payload) do data, _ = socket.recvfrom(65536) data.force_encoding(text.encoding) end let(:client) { UDPSocket.new } before do client.send(text, 0, host, port) end after do client.close end it "receives udp packets" do expect(payload).to(be == text) end end end describe "#tcp_listener" do subject { Flores::Random.tcp_listener } it_behaves_like Socket context "#accept" do let(:client) { TCPSocket.new(host, port) } before do client end after do client.close end it "returns a socket" do connection, _address = socket.accept expect(connection).to(be_a(Socket)) end end end end end flores-0.0.8/spec/flores/pki_integration_spec.rb0000644000175000017500000000410114537405540023063 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "spec_init" require "flores/pki" describe "PKI Integration" do let(:csr) { Flores::PKI::CertificateSigningRequest.new } # Here, I use a 1024-bit key for faster tests. # Please do not use such small keys in production. let(:key_bits) { 1024 } let(:key) { OpenSSL::PKey::RSA.generate(key_bits, 65537) } let(:certificate_duration) { Flores::Random.number(1..86400) } context "with self-signed client/server certificate" do let(:certificate_subject) { "CN=server.example.com" } let(:certificate) { csr.create } # Returns [socket, address, port] let(:listener) { Flores::Random.tcp_listener } let(:server) { listener[0] } let(:server_address) { listener[1] } let(:server_port) { listener[2] } let(:server_context) { OpenSSL::SSL::SSLContext.new } let(:client_context) { OpenSSL::SSL::SSLContext.new } before do #Thread.abort_on_exception = true csr.subject = certificate_subject csr.public_key = key.public_key csr.start_time = Time.now csr.expire_time = csr.start_time + certificate_duration csr.signing_key = key csr.want_signature_ability = true server_context.cert = certificate server_context.key = key server_context.ssl_version = :TLSv1 server_context.verify_mode = OpenSSL::SSL::VERIFY_NONE client_store = OpenSSL::X509::Store.new client_store.add_cert(certificate) client_context.cert_store = client_store client_context.verify_mode = OpenSSL::SSL::VERIFY_PEER client_context.ssl_version = :TLSv1 ssl_server = OpenSSL::SSL::SSLServer.new(server, server_context) Thread.new do begin ssl_server.accept rescue => e puts "Server accept failed: #{e}" end end end it "should successfully connect as a client" do socket = TCPSocket.new(server_address, server_port) ssl_client = OpenSSL::SSL::SSLSocket.new(socket, client_context) ssl_client.connect end end end flores-0.0.8/spec/flores/rspec/0000755000175000017500000000000014537405540017456 5ustar thegodtunethegodtuneflores-0.0.8/spec/flores/rspec/stress_spec.rb0000644000175000017500000000266114537405540022345 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "spec_init" Counter = Class.new do attr_reader :value def initialize @value = 0 end def incr @value += 1 end def decr @value -= 1 end end describe Flores::RSpec::Stress do subject { Counter.new } before do expect(subject.value).to(be == 0) subject.incr expect(subject.value).to(be == 1) end after do expect(subject.value).to(be == 1) subject.decr expect(subject.value).to(be == 0) end stress_it "should call all before and after hooks" do expect(subject.value).to(be == 1) end describe "level 1" do before do expect(subject.value).to(be == 1) subject.incr expect(subject.value).to(be == 2) end after do expect(subject.value).to(be == 2) subject.decr expect(subject.value).to(be == 1) end stress_it "should call all before and after hooks" do expect(subject.value).to(be == 2) end describe "level 2" do before do expect(subject.value).to(be == 2) subject.incr expect(subject.value).to(be == 3) end after do expect(subject.value).to(be == 3) subject.decr expect(subject.value).to(be == 2) end stress_it "should call all before and after hooks" do expect(subject.value).to(be == 3) end end end end flores-0.0.8/spec/flores/pki_spec.rb0000644000175000017500000000376314537405540020475 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "spec_init" require "flores/pki" describe Flores::PKI::CertificateSigningRequest do let(:csr) { Flores::PKI::CertificateSigningRequest.new } # Here, I use a 512-bit key for faster tests. # Please do not use 512-bit keys in production. let(:key_bits) { 512 } let(:key) { OpenSSL::PKey::RSA.generate(key_bits, 65537) } let(:certificate_duration) { Flores::Random.number(1..86400) } #before do #csr.subject = "OU=Fancy Pants Co." #csr.public_key = root_key.public_key #csr.start_time = Time.now #csr.expire_time = csr.start_time + certificate_duration #end shared_examples_for "a certificate" do it "returns a valid certificate" do expect(certificate).to(be_a(OpenSSL::X509::Certificate)) end end context "#subject=" do context "with an invalid subject" do let(:certificate_subject) { Flores::Random.text(1..20) } it "fails" do expect { csr.subject = certificate_subject }.to(raise_error(Flores::PKI::CertificateSigningRequest::InvalidSubject)) end end end context "a self-signed client/server certificate" do let(:certificate_subject) { "CN=server.example.com" } before do csr.subject = certificate_subject csr.public_key = key.public_key csr.start_time = Time.now csr.expire_time = csr.start_time + certificate_duration csr.signing_key = key end let(:certificate) { csr.create } it_behaves_like "a certificate" end end describe Flores::PKI do context ".random_serial" do let(:serial) { Flores::PKI.random_serial } stress_it "generates a valid OpenSSL::BN value" do OpenSSL::BN.new(serial) Integer(serial) end end context ".generate" do it "returns a certificate and a key" do certificate, key = Flores::PKI.generate expect(certificate).to(be_a(OpenSSL::X509::Certificate)) expect(key).to(be_a(OpenSSL::PKey::RSA)) end end end flores-0.0.8/spec/spec_init.rb0000644000175000017500000000041514537405540017352 0ustar thegodtunethegodtune# encoding: utf-8 # This file is part of ruby-flores. # Copyright (C) 2015 Jordan Sissel # require "simplecov" SimpleCov.start require "flores/random" require "flores/rspec" RSpec.configure do |config| Kernel.srand config.seed Flores::RSpec.configure(config) end flores-0.0.8/Gemfile0000644000175000017500000000026614537405540015415 0ustar thegodtunethegodtunesource "https://rubygems.org" group "development" do gem "rspec", ">= 3.0.0" gem "fuubar" gem "stud" gem "pry" end group "test" do gem 'simplecov', :require => false end flores-0.0.8/.rubocop.yml0000644000175000017500000000355414537405540016377 0ustar thegodtunethegodtune# Let's not argue over this... StringLiterals: Enabled: false # I can't find a reason for raise vs fail. SignalException: Enabled: false # I can't find a reason to prefer 'map' when 'collect' is what I mean. # I'm collecting things from a list. Maybe someone can help me understand the # semantics here. CollectionMethods: Enabled: false # Why do you even *SEE* trailing whitespace? Because your editor was # misconfigured to highlight trailing whitespace, right? Maybe turn that off? # ;) TrailingWhitespace: Enabled: false # Line length is another weird problem that somehow in the past 40 years of # computing we don't seem to have solved. It's a display problem :( LineLength: Max: 9000 # %w() vs [ "x", "y", ... ] # The complaint is on lib/pleaserun/detector.rb's map of OS=>Runner, # i'll ignore it. WordArray: MinSize: 5 # A 20-line method isn't too bad. MethodLength: Max: 20 # Hash rockets (=>) forever. Why? Not all of my hash keys are static symbols. HashSyntax: EnforcedStyle: hash_rockets # I prefer explicit return. It makes it clear in the code that the # code author intended to return a value from a method. RedundantReturn: Enabled: false # My view on a readable case statement seems to disagree with # what rubocop wants and it doesn't let me configure it other than # enable/disable. CaseIndentation: Enabled: false # module This::Module::Definition is good. Style/ClassAndModuleChildren: Enabled: true EnforcedStyle: compact # "in interpolation #{use.some("double quotes is ok")}" Style/StringLiteralsInInterpolation: Enabled: true EnforcedStyle: double_quotes # Long-block `if !something ... end` are more readable to me than `unless something ... end` Style/NegatedIf: Enabled: false Style/NumericLiterals: MinDigits: 6 # This kind of style "useless use of %x" assumes code is write-once. Style/UnneededPercentX: Enabled: false flores-0.0.8/README.md0000644000175000017500000001471614537405540015406 0ustar thegodtunethegodtune# Flores - a stress testing library This library is named in loving memory of Carlo Flores. --- When writing tests, it is often good to test a wide variety of inputs to ensure your entire input range behaves correctly. Further, adding a bit of randomness in your tests can help find bugs. ## Why Flores? Randomization helps you cover a wider range of inputs to your tests to find bugs. Stress testing (run a test repeatedly) helps you find bugs faster. We can use stress testing results to find common patterns in failures! Let's look at a sample situation. Ruby's TCPServer. Let's write a spec to cover a spec covering port binding: ```ruby require "flores/rspec" RSpec.configure do |config| Flores::RSpec.configure(config) end describe TCPServer do subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) } let(:port) { 5000 } let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") } after { socket.close } it "should bind successfully" do socket.bind(sockaddr) expect(socket.local_address.ip_port).to(be == port) end end ``` Running it: ``` % rspec tcpserver_spec.rb . Finished in 0.00248 seconds (files took 0.16294 seconds to load) 1 example, 0 failures ``` That's cool. We now have some confidence that TCPServer on port 5000 will bind successfully. What about the other ports? What ranges of values should work? What shouldn't? Let's assume I don't know anything about tcp port ranges and test randomly in the range -100,000 to +100,000: ```ruby describe TCPServer do let(:port) { Flores::Random.integer(-100_000..100_000) } ... end ``` Running it: ``` % rspec tcpserver_spec.rb F Failures: 1) TCPServer should bind successfully Failure/Error: expect(socket.local_address.ip_port).to(be == port) expected: == 70144 got: 4608 # ./tcpserver_spec.rb:18:in `block (2 levels) in ' Finished in 0.00163 seconds (files took 0.09982 seconds to load) 1 example, 1 failure Failed examples: rspec ./tcpserver_spec.rb:16 # TCPServer should bind successfully ``` Well that's weird. Binding port 70144 actually made it bind on port 4608! If we run it more times, we'll see all kinds of different results: * Run 1: ``` Failure/Error: expect(socket.local_address.ip_port).to(be == port) expected: == 83359 got: 17823 ``` * Run 2: ``` Failure/Error: let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") } SocketError: getaddrinfo: nodename nor servname provided, or not known ``` * Run 3: ``` Errno::EACCES: Permission denied - bind(2) for 127.0.0.1:615 ``` * Run 4: ``` Finished in 0.00161 seconds (files took 0.10356 seconds to load) 1 example, 0 failures ``` ## Analyze the results The above example showed that there were many different kinds of failures when we introduced randomness to our test inputs. We can go further and run a given spec example many times and group the failures by similarity and include context (what the inputs were, etc) This library provides an `stress_it` helper which behaves similarly to rspec's `it` except that the spec is copied (and run) many times. The result is grouped by failure and includes context (`let` and `subject`). Let's see how it works: We'll change `it` to use `stress_it` instead, and also add `analyze_results`: ```diff - it "should bind successfully" do + analyze_results # track the `let` and `subject` values in our tests. + stress_it "should bind successfully" do ``` The `analyze_results` method just adds an `after` hook to capture the `let` and `subject` values used in each example. The final step is to use a custom formatter provided with this library to do the analysis. Now rerunning the test. With barely any spec changes from the original, we have now enough randomness and stress testing to identify many different failure cases and input ranges for those failures. ``` % rspec -f Flores::RSpec::Formatters::Analyze tcpserver_spec.rb TCPServer should bind successfully 33.96% (of 742 total) tests are successful Failure analysis: 46.90% -> [348] SocketError Sample exception for {:socket=>#, :port=>-74235} getaddrinfo: nodename nor servname provided, or not known Samples causing SocketError: {:socket=>#, :port=>-60170} {:socket=>#, :port=>-73159} {:socket=>#, :port=>-84648} {:socket=>#, :port=>-5936} {:socket=>#, :port=>-78195} 18.33% -> [136] RSpec::Expectations::ExpectationNotMetError Sample exception for {:socket=>#, :port=>72849, :sockaddr=>"\x10\x02\x1C\x91\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} expected: == 72849 got: 7313 Samples causing RSpec::Expectations::ExpectationNotMetError: {:socket=>#, :port=>74072, :sockaddr=>"\x10\x02!X\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>77973, :sockaddr=>"\x10\x020\x95\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>88867, :sockaddr=>"\x10\x02[#\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>87710, :sockaddr=>"\x10\x02V\x9E\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>95690, :sockaddr=>"\x10\x02u\xCA\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} 0.81% -> [6] Errno::EACCES Sample exception for {:socket=>#, :port=>65897, :sockaddr=>"\x10\x02\x01i\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} Permission denied - bind(2) for 127.0.0.1:361 Samples causing Errno::EACCES: {:socket=>#, :port=>879, :sockaddr=>"\x10\x02\x03o\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>66258, :sockaddr=>"\x10\x02\x02\xD2\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>65829, :sockaddr=>"\x10\x02\x01%\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>66044, :sockaddr=>"\x10\x02\x01\xFC\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} {:socket=>#, :port=>65897, :sockaddr=>"\x10\x02\x01i\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"} Finished in 0.10509 seconds 742 examples, 490 failures ``` Now we can see a wide variety of failure cases all found through randomization. Nice! flores-0.0.8/Gemfile.lock0000644000175000017500000000212214537405540016335 0ustar thegodtunethegodtuneGEM remote: https://rubygems.org/ specs: coderay (1.1.0) diff-lcs (1.2.5) ffi (1.9.6-java) fuubar (2.0.0) rspec (~> 3.0) ruby-progressbar (~> 1.4) method_source (0.8.2) multi_json (1.11.1) pry (0.10.1) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) pry (0.10.1-java) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) spoon (~> 0.0) rspec (3.2.0) rspec-core (~> 3.2.0) rspec-expectations (~> 3.2.0) rspec-mocks (~> 3.2.0) rspec-core (3.2.0) rspec-support (~> 3.2.0) rspec-expectations (3.2.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-mocks (3.2.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-support (3.2.1) ruby-progressbar (1.7.1) simplecov (0.6.4) multi_json (~> 1.0) simplecov-html (~> 0.5.3) simplecov-html (0.5.3) slop (3.6.0) spoon (0.0.4) ffi stud (0.0.19) PLATFORMS java ruby DEPENDENCIES fuubar pry rspec (>= 3.0.0) simplecov stud