succubus 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  _A random generator based on a generalised Backus-Naur Form grammar_
2
2
 
3
- [![Build Status](https://travis-ci.org/asilano/succubus.png?branch=master)](https://travis-ci.org/asilano/succubus)
3
+ [![Build Status](https://travis-ci.org/asilano/succubus.png?branch=master)](https://travis-ci.org/asilano/succubus)[![Coverage Status](https://coveralls.io/repos/asilano/succubus/badge.png?branch=master)](https://coveralls.io/r/asilano/succubus)
4
4
 
5
5
  **Succubus** is a generator which takes stochastic paths through a
6
6
  generalised Backus-Naur Form grammar to produce random text. For instance, the following:
data/Rakefile CHANGED
@@ -3,7 +3,9 @@ require 'bundler/gem_tasks'
3
3
 
4
4
  Rake::TestTask.new do |t|
5
5
  t.test_files = FileList['test/**/*_test.rb', 'spec/**/*_spec.rb']
6
- t.warning = true
6
+
7
+ # Disable warnings - coveralls spews out a bunch
8
+ # t.warning = true
7
9
  end
8
10
 
9
11
  task :default => :test
@@ -1,77 +1,43 @@
1
+ require 'succubus/grammar'
2
+ require 'succubus/generator'
3
+ require 'succubus/result'
4
+
5
+ # @author Chris Howlett
1
6
  module Succubus
7
+ # Raised by {Grammar#initialize Grammar.new} if the grammar defined in the
8
+ # supplied block cannot be parsed.
9
+ #
10
+ # @!attribute [r] errors
11
+ # @return [Array<String>] the errors encountered during parsing
2
12
  class ParseError < RuntimeError
3
- attr_accessor :errors
4
- end
5
-
6
- class ExecuteError < RuntimeError
7
- attr_accessor :errors, :partial
8
- end
9
-
10
- class Grammar
11
- def initialize(&block)
12
- @rules = {}
13
- @errors = []
14
- define_singleton_method(:create, block)
15
- create
16
- class << self ; undef_method :create ; end
17
-
18
- unless @errors.empty?
19
- pe = ParseError.new("Errors found parsing")
20
- pe.errors = @errors
21
- raise pe
22
- end
23
- end
13
+ attr_reader :errors
24
14
 
25
- def add_rule(name, *choices, &block)
26
- name = name.to_sym
27
- @errors << "Duplicate rule definition: #{name}" if @rules.include? name
28
- @rules[name] = choices
29
- end
30
-
31
- def execute(start)
32
- gen = Generator.new(@rules)
33
- gen.run(start)
34
-
35
- unless gen.errors.empty?
36
- ee = ExecuteError.new("Errors found executing")
37
- ee.errors = gen.errors
38
- ee.partial = gen.result
39
- raise ee
40
- end
41
-
42
- gen.result
15
+ # @private
16
+ def set_errors(errors)
17
+ @errors = errors
43
18
  end
44
19
  end
45
-
46
- class Generator
47
- attr_reader :result
48
- attr_reader :errors
49
-
50
- def initialize(rules)
51
- @result = ""
52
- @rules = rules
53
- @errors = []
54
- end
55
-
56
- def run(rule)
57
- @result = invoke(rule)
20
+
21
+ # Raised by {Grammar#execute} if the {Generator} encountered any errors
22
+ # while executing the grammar.
23
+ #
24
+ # @!attribute [r] errors
25
+ # @return [Array<String>] the errors encountered during execution
26
+ # @!attribute [r] partial
27
+ # @return [Result] as much of the result as the {Generator} was able
28
+ # to produce. Typically, this will be a string with missing rules
29
+ # surrounded by +!!missing!!+. For instance, +"My !!pet!! is nice"+
30
+ class ExecuteError < RuntimeError
31
+ attr_reader :errors, :partial
32
+
33
+ # @private
34
+ def set_errors(errors)
35
+ @errors = errors
58
36
  end
59
-
60
- def invoke(rule)
61
- unless @rules.include? rule
62
- @errors << "No such rule: #{rule}"
63
- return "!!#{rule}!!"
64
- end
65
-
66
- local_res = ""
67
- @rules[rule].sample.scan(/(.*?(?<!\\)(?:\\\\)*)(<.*?>|$)/) do |match|
68
- local_res << match[0]
69
- unless match[1].empty?
70
- local_res << invoke(match[1].match(/<(.*?)>/)[1].to_sym)
71
- end
72
- end
73
-
74
- return local_res
37
+
38
+ # @private
39
+ def set_partial(partial)
40
+ @partial = partial
75
41
  end
76
42
  end
77
- end
43
+ end
@@ -0,0 +1,82 @@
1
+ module Succubus
2
+ # Class which handles running through a {Grammar} and producing a
3
+ # {Result} string.
4
+ #
5
+ # @private
6
+ class Generator
7
+
8
+ # Run through a given {Grammar}, starting at a given rule. Optionally
9
+ # use a given random seed.
10
+ #
11
+ # @param [Grammar] grammar the {Grammar} to generate a string from
12
+ # @param [Symbol] rule the name of the rule to start generation from
13
+ # @param [Integer] seed optional random seed. Defaults to +nil+, which will
14
+ # make the {Generator} use a random seed
15
+ def self.run(grammar, rule, seed=nil)
16
+ new(grammar, seed).run(rule)
17
+ end
18
+
19
+ # Create a new {Generator}, ready to run through the supplied {Grammar},
20
+ # optionally with the given random seed.
21
+ #
22
+ # @param [Grammar] grammar the {Grammar} to generate a string from
23
+ # @param [Integer] seed optional random seed. Defaults to +nil+, which will
24
+ # make the {Generator} use a random seed
25
+ def initialize(grammar, seed=nil)
26
+ @rules = grammar.rules
27
+
28
+ @seed = seed
29
+ unless @seed
30
+ srand()
31
+ @seed = rand(0xffffffff)
32
+ end
33
+ srand(@seed)
34
+
35
+ @errors = []
36
+ end
37
+
38
+ # Produce a random {Result} string from the Generator's {Grammar}
39
+ #
40
+ # @param [Symbol] rule the name of the rule to start generation from
41
+ # @return [Result] the {Result} string produced
42
+ def run(rule)
43
+ @result = Result.new(@seed, invoke(rule))
44
+ @result.set_errors(@errors)
45
+
46
+ return @result
47
+ end
48
+
49
+ private
50
+ # Execute a single rule all the way down, returning the +String+ randomly
51
+ # produced from it
52
+ #
53
+ # @param [Symbol] rule the name of the rule to execute
54
+ # @return [String] the string produced
55
+ def invoke(rule)
56
+ # Check the rule is defined; error if so
57
+ unless @rules.include? rule
58
+ @errors << "No such rule: #{rule}"
59
+ return "!!#{rule}!!"
60
+ end
61
+
62
+ local_res = ""
63
+
64
+ # Pick an option. We can't use Array#sample, as its implementation differs
65
+ # between Ruby versions, so random seeds aren't portable.
66
+ choices = @rules[rule]
67
+ choice = choices[rand(choices.length)]
68
+
69
+ # Scan it for all instances of (unescaped) <rules>
70
+ # Each match will be an array containing the portion of string before
71
+ # the <rule>; and the <rule> itself (which may be empty on the last match)
72
+ choice.scan(/(.*?(?<!\\)(?:\\\\)*)(<.*?>|$)/) do |match|
73
+ local_res << match[0]
74
+ unless match[1].empty?
75
+ local_res << invoke(match[1].match(/<(.*?)>/)[1].to_sym)
76
+ end
77
+ end
78
+
79
+ return local_res
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,79 @@
1
+ module Succubus
2
+ # Class which contains the rules describing a grammar from
3
+ # which to generate random strings.
4
+ #
5
+ # {Grammar} provides the vast majority of the user interface
6
+ # to Succubus.
7
+ #
8
+ # @!attribute [r] last_seed
9
+ # @return [Integer] the random seed used by the last call to {#execute}
10
+ # @!attribute [r] rules
11
+ # @return [Hash<Symbol => String>] the rules describing this grammar
12
+ class Grammar
13
+ attr_reader :last_seed
14
+ attr_reader :rules
15
+
16
+ # Create a new {Grammar} object. {#initialize Grammar.new} should be passed
17
+ # a block defining the rules for the grammar. +self+ in the block will be
18
+ # the newly-created {Grammar} instance
19
+ #
20
+ # @yield Block defining the grammar. +self+ is the newly-created {Grammar},
21
+ # so bare calls to {#add_rule} work as expected.
22
+ # @raise [ParseError] if errors were encountered parsing the grammar in the
23
+ # supplied block
24
+ def initialize(&block)
25
+ @rules = {}
26
+ @errors = []
27
+ define_singleton_method(:create, block)
28
+ create
29
+ class << self ; undef_method :create ; end
30
+
31
+ unless @errors.empty?
32
+ pe = ParseError.new("Errors found parsing")
33
+ pe.set_errors(@errors)
34
+ raise pe
35
+ end
36
+ end
37
+
38
+ # Define a new rule into the {Grammar}.
39
+ #
40
+ # {#add_rule} is usually called from the block parameter of {#initialize Grammar.new};
41
+ # however, it can be called on an existing {Grammar} object. Errors encountered by
42
+ # {#add_rule} will only be reported when called via {#initialize Grammar.new}.
43
+ #
44
+ # @param [#to_sym] name name of the rule
45
+ # @param [Array<String>] choices possible replacements for the rule. When +<name>+ is
46
+ # encountered during execution of the {Grammar}, +<name>+ will be replaced by one of
47
+ # the strings in +choices+ chosen uniformly at random.
48
+ def add_rule(name, *choices)
49
+ name = name.to_sym
50
+ @errors << "Duplicate rule definition: #{name}" if @rules.include? name
51
+ @rules[name] = choices
52
+ end
53
+
54
+ # Execute the grammar, producing a random {Result} string.
55
+ #
56
+ # The rule named +start+ is examined, and one of that rule's choices chosen. Then, for each
57
+ # rule reference tag +<name>+ in that choice, the tag is recursively replaced with one of
58
+ # the choices for the rule named +name+.
59
+ #
60
+ # @param [#to_sym] start name of the rule to start execution at
61
+ # @param [Integer] seed optional random seed. Defaults to +nil+, which will
62
+ # cause a random seed to be used
63
+ #
64
+ # @raise [ExecuteError] if errors were encountered while executing the grammar
65
+ def execute(start, seed=nil)
66
+ result = Generator.run(self, start.to_sym, seed)
67
+
68
+ unless result.errors.empty?
69
+ ee = ExecuteError.new("Errors found executing")
70
+ ee.set_errors(result.errors)
71
+ ee.set_partial(result)
72
+ raise ee
73
+ end
74
+
75
+ @last_seed = result.random_seed
76
+ result
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,39 @@
1
+ module Succubus
2
+ # Class containing the result of executing a {Grammar}.
3
+ #
4
+ # Result is a subclass of String, so it can be used exactly as if it were a String.
5
+ # Its main purpose is to provide access to metadata about the execution that
6
+ # produced it - namely the +random_seed+ used, and any +errors+ produced.
7
+ #
8
+ # @!attribute [r] random_seed
9
+ # @return [Integer] The random seed used to generate this Result.
10
+ # {#random_seed} can be passed to {Grammar#execute} to exactly repeat the process
11
+ # that generated this {Result} (for possible debugging purposes, or reporting issues)
12
+ # @!attribute [r] errors
13
+ # @return [Array<String>] Any errors that occurred during the generation
14
+ # of this {Result}.
15
+ class Result < String
16
+ attr_reader :random_seed, :errors
17
+
18
+ # Create a new Result
19
+ #
20
+ # @private
21
+ #
22
+ # @param [Integer] seed The random seed used to generate this result
23
+ # @param [Array] args Passed through to +super+
24
+ # @param [Proc] block Passed through to +super+
25
+ def initialize(seed, *args, &block)
26
+ @random_seed = seed
27
+ super(*args, &block)
28
+ end
29
+
30
+ # Set the errors encountered creating this Result
31
+ #
32
+ # @private
33
+ #
34
+ # @param [Array<String>] errors the errors encountered
35
+ def set_errors(errors)
36
+ @errors = errors
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,4 @@
1
1
  module Succubus
2
- VERSION = "0.0.0"
2
+ # The gem version
3
+ VERSION = "0.1.0"
3
4
  end
@@ -1,5 +1,4 @@
1
- require 'minitest/autorun'
2
- require File.dirname(__FILE__) + '/../test/support/fixed_random'
1
+ require File.dirname(__FILE__) + '/spec_helper'
3
2
  require 'succubus'
4
3
 
5
4
  describe Succubus::Grammar do
@@ -50,13 +49,27 @@ describe Succubus::Grammar do
50
49
  end
51
50
 
52
51
  it "should produce a known string given pre-chosen randomness" do
53
- expect_sample ["I have a <colour> <pet>", "<pet>s look best in <colour>"], "<pet>s look best in <colour>"
54
- expect_sample ["cat", "dog"], "cat"
55
- expect_sample ["black", "white"], "black"
52
+ # From ["I have a <colour> <pet>", "<pet>s look best in <colour>"], choose "<pet>s look best in <colour>"
53
+ expect_random 2, 1
54
+ # From ["cat", "dog"], choose "cat"
55
+ expect_random 2, 0
56
+ # From ["black", "white"], choose "black"
57
+ expect_random 2, 0
56
58
 
57
59
  @grammar.execute(:base).must_equal "cats look best in black"
58
60
 
59
- samples_must_be_used
61
+ queue_must_be_used
62
+ end
63
+
64
+ it "should always produce the same string given a known seed" do
65
+ result = @grammar.execute(:base)
66
+ result.random_seed.wont_be_nil
67
+
68
+ seed = result.random_seed
69
+ 100.times { @grammar.execute(:base, seed).must_equal result }
70
+
71
+ # We know from manual runs that the seed 42 produces "I have a white cat"
72
+ 100.times { @grammar.execute(:base, 42).must_equal "I have a white cat" }
60
73
  end
61
74
  end
62
75
 
@@ -72,7 +85,7 @@ describe Succubus::Grammar do
72
85
  end
73
86
 
74
87
  ex = bad_parse.must_raise Succubus::ParseError
75
- ex.errors.must_have_same_elements_as ["Duplicate rule definition: colour", "Duplicate rule definition: base"]
88
+ ex.errors.must_equal_contents ["Duplicate rule definition: colour", "Duplicate rule definition: base"]
76
89
  end
77
90
 
78
91
  it "should fail to execute when rules are missing" do
@@ -81,10 +94,11 @@ describe Succubus::Grammar do
81
94
  add_rule :colour, "black", "white"
82
95
  end
83
96
 
84
- expect_sample %w<black white>, "black"
97
+ # From ["black", "white"], choose "black"
98
+ expect_random 2, 0
85
99
 
86
100
  ex = proc {grammar.execute(:base)}.must_raise Succubus::ExecuteError
87
- ex.errors.must_have_same_elements_as ["No such rule: size", "No such rule: pet"]
101
+ ex.errors.must_equal_contents ["No such rule: size", "No such rule: pet"]
88
102
  ex.partial.must_equal "I have a !!size!! black !!pet!!"
89
103
  end
90
104
  end
@@ -0,0 +1,27 @@
1
+ require 'coveralls'
2
+ Coveralls.wear! do
3
+ add_filter '/test/'
4
+ add_filter '/spec/'
5
+ add_filter '/examples/'
6
+ end
7
+ require 'minitest/autorun'
8
+ require 'minitest/great_expectations'
9
+
10
+ if false
11
+ require 'turn'
12
+ Turn.config do |c|
13
+ # use one of output formats:
14
+ # :outline - turn's original case/test outline mode [default]
15
+ # :progress - indicates progress with progress bar
16
+ # :dotted - test/unit's traditional dot-progress mode
17
+ # :pretty - new pretty reporter
18
+ # :marshal - dump output as YAML (normal run mode only)
19
+ # :cue - interactive testing
20
+ c.format = :outline
21
+ # turn on invoke/execute tracing, enable full backtrace
22
+ c.trace = true
23
+ # use humanized test names (works only with :outline format)
24
+ c.natural = true
25
+ end
26
+ end
27
+ require File.dirname(__FILE__) + '/../test/support/fixed_random'
@@ -1,40 +1,39 @@
1
- require File.dirname(__FILE__) + '/subset_asserts'
2
-
3
1
  module MiniTest::Assertions
4
- @@queued_samples = []
2
+ @@queued_random = []
5
3
 
6
- def self.queued_samples; @@queued_samples; end
7
- def self.next_sample; @@queued_samples.shift; end
4
+ def self.queued_random; @@queued_random; end
5
+ def self.next_random; @@queued_random.shift; end
8
6
 
9
- def expect_sample(options, *choice)
10
- assert_subset choice, options, "Can't queue #{options.inspect}.sample => #{choice}; choice not in options"
11
- @@queued_samples << {:options => options, :choice => choice}
7
+ def expect_random(max, choice)
8
+ assert_includes (0...max), choice, "Can't queue rand(#{max.inspect}) => #{choice}; choice not in [0-1)"
9
+ unless max.nil?
10
+ assert_kind_of Integer, choice, "Can't queue rand(#{max}) => #{choice}; choice not an integer"
11
+ end
12
+ @@queued_random << {:max => max, :choice => choice}
12
13
  end
13
14
 
14
- def samples_must_be_used
15
- assert_empty @@queued_samples, "Expected to have used all queued samples: #{@@queued_samples.length} left"
15
+ def queue_must_be_used
16
+ assert_empty @@queued_random, "Expected to have used all queued random choices: #{@@queued_random.length} left"
16
17
  end
17
18
  end
18
19
 
19
- unless Array.method_defined? :sample_with_predefined_values
20
- class Array
21
- def sample_with_predefined_values(n = nil, &block)
22
- queued = MiniTest::Assertions.queued_samples
23
- if !queued.empty? && queued[0][:options] == self && queued[0][:choice].length == (n || 1)
24
- expected = MiniTest::Assertions.next_sample
25
- ret = expected[:choice]
26
- ret = ret[0] if n.nil?
27
- return ret
20
+ unless Kernel.method_defined? :rand_with_predefined_values
21
+ module Kernel
22
+ def rand_with_predefined_values(max=nil, &block)
23
+ queued = MiniTest::Assertions.queued_random
24
+ if !queued.empty? && queued[0][:max] == max
25
+ expected = MiniTest::Assertions.next_random
26
+ return expected[:choice]
28
27
  else
29
- if n
30
- sample_without_predefined_values(n, &block)
28
+ if max
29
+ rand_without_predefined_values(max, &block)
31
30
  else
32
- sample_without_predefined_values(&block)
31
+ rand_without_predefined_values(&block)
33
32
  end
34
33
  end
35
34
  end
36
- alias_method :sample_without_predefined_values, :sample
37
- alias_method :sample, :sample_with_predefined_values
35
+ alias_method :rand_without_predefined_values, :rand
36
+ alias_method :rand, :rand_with_predefined_values
38
37
 
39
38
  end
40
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: succubus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-11 00:00:00.000000000 Z
12
+ date: 2013-04-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: !ruby/object:Gem::Requirement
16
+ requirement: &20243712 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,15 +21,32 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
24
+ version_requirements: *20243712
25
+ - !ruby/object:Gem::Dependency
26
+ name: minitest
27
+ requirement: &20243460 !ruby/object:Gem::Requirement
25
28
  none: false
26
29
  requirements:
27
30
  - - ! '>='
28
31
  - !ruby/object:Gem::Version
29
32
  version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *20243460
30
36
  - !ruby/object:Gem::Dependency
31
- name: minitest
32
- requirement: !ruby/object:Gem::Requirement
37
+ name: minitest-great_expectations
38
+ requirement: &20243208 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *20243208
47
+ - !ruby/object:Gem::Dependency
48
+ name: turn
49
+ requirement: &20242956 !ruby/object:Gem::Requirement
33
50
  none: false
34
51
  requirements:
35
52
  - - ! '>='
@@ -37,12 +54,18 @@ dependencies:
37
54
  version: '0'
38
55
  type: :development
39
56
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
57
+ version_requirements: *20242956
58
+ - !ruby/object:Gem::Dependency
59
+ name: coveralls
60
+ requirement: &20242704 !ruby/object:Gem::Requirement
41
61
  none: false
42
62
  requirements:
43
63
  - - ! '>='
44
64
  - !ruby/object:Gem::Version
45
65
  version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *20242704
46
69
  description: ! " Succubus is a generator which takes stochastic paths through a
47
70
  \n generalised Backus-Naur Form grammar to produce random text.\n\n See the
48
71
  examples for details.\n"
@@ -51,13 +74,16 @@ executables: []
51
74
  extensions: []
52
75
  extra_rdoc_files: []
53
76
  files:
77
+ - lib/succubus/generator.rb
78
+ - lib/succubus/grammar.rb
79
+ - lib/succubus/result.rb
54
80
  - lib/succubus/version.rb
55
81
  - lib/succubus.rb
56
82
  - Rakefile
57
83
  - README.md
58
84
  - test/support/fixed_random.rb
59
- - test/support/subset_asserts.rb
60
85
  - spec/grammar_spec.rb
86
+ - spec/spec_helper.rb
61
87
  homepage: https://github.com/asilano/succubus
62
88
  licenses: []
63
89
  post_install_message:
@@ -78,11 +104,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
104
  version: '0'
79
105
  requirements: []
80
106
  rubyforge_project:
81
- rubygems_version: 1.8.24
107
+ rubygems_version: 1.8.16
82
108
  signing_key:
83
109
  specification_version: 3
84
110
  summary: A random generator based on a generalised Backus-Naur Form grammar
85
111
  test_files:
86
112
  - test/support/fixed_random.rb
87
- - test/support/subset_asserts.rb
88
113
  - spec/grammar_spec.rb
114
+ - spec/spec_helper.rb
@@ -1,49 +0,0 @@
1
- module MiniTest::Assertions
2
- def assert_subset(subset, superset, msg = nil)
3
- sub = subset.to_a.dup
4
- supe = superset.to_a.dup
5
- msg ||= "#{sub.inspect} is not a subset of #{supe.inspect}"
6
- failed = false
7
- sub.each do |elem|
8
- if supe.index(elem)
9
- supe.delete_at(supe.index(elem) || li.length)
10
- else
11
- failed = true
12
- break
13
- end
14
- end
15
-
16
- assert(!failed, msg)
17
- end
18
-
19
- def assert_disjoint(left, right, msg = nil)
20
- left_a = left.to_a.dup
21
- right_a = right.to_a.dup
22
- msg ||= "#{left_a.inspect} and #{right_a.inspect} are not disjoint"
23
- failed = false
24
- left_a.each do |elem|
25
- if right_a.index(elem)
26
- failed = true
27
- break
28
- end
29
- end
30
-
31
- assert(!failed, msg)
32
- end
33
-
34
- def assert_same_elements(a1, a2, msg = nil)
35
- [:select, :inject, :size].each do |m|
36
- [a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
37
- end
38
-
39
- assert a1h = a1.inject({}) { |h,e| h[e] = a1.select { |i| i == e }.size; h }
40
- assert a2h = a2.inject({}) { |h,e| h[e] = a2.select { |i| i == e }.size; h }
41
-
42
- assert_equal(a1h, a2h, msg)
43
- end
44
- end
45
-
46
- module MiniTest::Expectations
47
- infect_an_assertion :assert_subset, :must_be_subset_of
48
- infect_an_assertion :assert_same_elements, :must_have_same_elements_as
49
- end