pairwise_psych 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +34 -0
- data/bin/pairwise +7 -0
- data/lib/pairwise/cli.rb +92 -0
- data/lib/pairwise/formatter/csv.rb +17 -0
- data/lib/pairwise/formatter/cucumber.rb +52 -0
- data/lib/pairwise/formatter.rb +1 -0
- data/lib/pairwise/input_data.rb +25 -0
- data/lib/pairwise/input_file.rb +62 -0
- data/lib/pairwise/ipo/horizontal.rb +27 -0
- data/lib/pairwise/ipo/vertical.rb +15 -0
- data/lib/pairwise/ipo.rb +63 -0
- data/lib/pairwise/pair_collection.rb +72 -0
- data/lib/pairwise/test_pair.rb +48 -0
- data/lib/pairwise.rb +40 -0
- data/lib/version.rb +3 -0
- data/spec/pairwise/cli_spec.rb +68 -0
- data/spec/pairwise/ipo/horizontal_spec.rb +42 -0
- data/spec/pairwise/ipo_spec.rb +7 -0
- data/spec/pairwise/pair_collection_spec.rb +15 -0
- data/spec/pairwise_spec.rb +123 -0
- data/spec/spec_helper.rb +8 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f3235ee705e199c82b95973eecb55348db8f8092
|
4
|
+
data.tar.gz: 0f69362bfa95968b1c87bccc462cef0168d792df
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1dd0513ecc088ec79b1dcd49d14513f582d77a2549659c3e091ff2b674d04389e6b01943527e70fc318c80407f660ed21b52a86a6b67a790ae6419a8d46ece72
|
7
|
+
data.tar.gz: af9493453403096987d4f4b0837fe3f672f0a6d34b7743c9dcf5b3409a16eb881912211e10b14a095d2f4b375ad0871d48a1ae124b49071faeb8fb989382b12b
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
Pairwise
|
2
|
+
-------
|
3
|
+
|
4
|
+
Pairwise is a Ruby based tool for selecting a smaller number of test input combinations (using pairwise generation)
|
5
|
+
rather than exhaustively testing all possible permutations.
|
6
|
+
|
7
|
+
Created by Joseph Wilks, updated by Ali King for newer Rubies
|
8
|
+
|
9
|
+
How to use Pairwise: http://josephwilk.github.com/pairwise/
|
10
|
+
|
11
|
+
Newer Rubies and Syck vs Psych
|
12
|
+
-----------
|
13
|
+
Syck and Psych are YAML serialization libraries. Historically Ruby used syck, now Psych is the default. There are a
|
14
|
+
couple of differences between the two, which are better described here :- http://devblog.arnebrasseur.net/2014-02-yaml-syck-vs-psych
|
15
|
+
|
16
|
+
This fork from the original pairwise just removes a directive to use syck and adds some tests for multibyte characters
|
17
|
+
which may have been interpreted differently by the different libraries.
|
18
|
+
|
19
|
+
Also adds unicode-display_width gem to help with formatting - multibyte characters are tricksy.
|
20
|
+
|
21
|
+
Tested on ruby 1.8.7-p374 and 2.0.0-p353 as a sampling, but this should be future-proof.
|
22
|
+
|
23
|
+
|
24
|
+
Running tests
|
25
|
+
------------
|
26
|
+
<pre><code>rake</code></pre
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
Copyright
|
31
|
+
--------
|
32
|
+
|
33
|
+
See LICENSE for details.
|
34
|
+
|
data/bin/pairwise
ADDED
data/lib/pairwise/cli.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
class Cli
|
5
|
+
BUILTIN_FORMATS = {
|
6
|
+
'cucumber' => [Pairwise::Formatter::Cucumber,
|
7
|
+
'Tables for Cucumber'],
|
8
|
+
'csv' => [Pairwise::Formatter::Csv,
|
9
|
+
'Comma seperated values']}
|
10
|
+
|
11
|
+
max = BUILTIN_FORMATS.keys.map{|s| s.length}.max
|
12
|
+
FORMAT_HELP = (BUILTIN_FORMATS.keys.sort.map do |key|
|
13
|
+
" #{key}#{' ' * (max - key.length)} : #{BUILTIN_FORMATS[key][1]}"
|
14
|
+
end)
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def execute(args)
|
18
|
+
new(args).execute!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(args, out = STDOUT)
|
23
|
+
@args, @out = args, out
|
24
|
+
@options = defaults
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse!
|
28
|
+
@args.extend(::OptionParser::Arguable)
|
29
|
+
@args.options do |opts|
|
30
|
+
opts.banner = ["Usage: pairwise [options] FILE.[yml|csv]", "",
|
31
|
+
"Example:",
|
32
|
+
"pairwise data/inputs.yml", "", "",
|
33
|
+
].join("\n")
|
34
|
+
opts.on("-k", "--keep-wild-cards",
|
35
|
+
"Don't automatically replace any wild-cards which appear",
|
36
|
+
"in the pairwise data") do
|
37
|
+
@options[:keep_wild_cards] = true
|
38
|
+
end
|
39
|
+
opts.on('-f FORMAT', '--format FORMAT',
|
40
|
+
"How to format pairwise data (Default: cucumber). Available formats:",
|
41
|
+
*FORMAT_HELP) do |format|
|
42
|
+
@options[:format] = format
|
43
|
+
end
|
44
|
+
opts.on_tail("--version", "Show version.") do
|
45
|
+
@out.puts Pairwise::VERSION
|
46
|
+
Kernel.exit(0)
|
47
|
+
end
|
48
|
+
opts.on_tail("-h", "--help", "You're looking at it.") do
|
49
|
+
exit_with_help
|
50
|
+
end
|
51
|
+
end.parse!
|
52
|
+
|
53
|
+
@filename_with_path = @args[0] unless @args.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute!
|
57
|
+
parse!
|
58
|
+
validate_options!
|
59
|
+
|
60
|
+
if inputs = InputFile.load(@filename_with_path)
|
61
|
+
builder = Pairwise::IPO.new(inputs.data, @options)
|
62
|
+
|
63
|
+
formatter.display(builder.build, inputs.labels)
|
64
|
+
else
|
65
|
+
puts "Error: '#{@filename_with_path}' does not contain the right structure for me to generate the pairwise set!"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def defaults
|
71
|
+
{ :keep_wild_cards => false,
|
72
|
+
:format => 'cucumber' }
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_options!
|
76
|
+
exit_with_help if @filename_with_path.nil? || @filename_with_path.empty?
|
77
|
+
raise Errno::ENOENT, @filename_with_path unless File.exist?(@filename_with_path)
|
78
|
+
exit_with_help unless File.file?(@filename_with_path)
|
79
|
+
end
|
80
|
+
|
81
|
+
def exit_with_help
|
82
|
+
@out.puts @args.options.help
|
83
|
+
Kernel.exit(0)
|
84
|
+
end
|
85
|
+
|
86
|
+
def formatter
|
87
|
+
format = BUILTIN_FORMATS[@options[:format]][0]
|
88
|
+
format.new(@out)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pairwise
|
2
|
+
module Formatter
|
3
|
+
class Csv
|
4
|
+
|
5
|
+
def initialize(out)
|
6
|
+
@out = out
|
7
|
+
end
|
8
|
+
|
9
|
+
def display(test_data, input_labels)
|
10
|
+
@out.puts input_labels.join(',')
|
11
|
+
test_data.each do |data|
|
12
|
+
@out.puts data.join(',')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'unicode/display_width'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
module Formatter
|
5
|
+
class Cucumber
|
6
|
+
|
7
|
+
def initialize(out)
|
8
|
+
@out = out
|
9
|
+
@max = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def display(test_data, input_labels)
|
13
|
+
@test_data = label_wild_cards(test_data, input_labels)
|
14
|
+
@input_labels = input_labels
|
15
|
+
|
16
|
+
@out.print "|"
|
17
|
+
@input_labels.each_with_index do |label, column|
|
18
|
+
@out.print padded_string(label, column) + "|"
|
19
|
+
end
|
20
|
+
@out.puts
|
21
|
+
|
22
|
+
@test_data.each do |data|
|
23
|
+
@out.print "|"
|
24
|
+
data.each_with_index do |datum, column|
|
25
|
+
@out.print padded_string(datum, column) + "|"
|
26
|
+
end
|
27
|
+
@out.puts
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def label_wild_cards(test_data, labels)
|
33
|
+
test_data.map do |data|
|
34
|
+
data.enum_for(:each_with_index).map do |datum, column|
|
35
|
+
datum == IPO::WILD_CARD ? "any_value_of_#{labels[column]}" : datum
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def padded_string(string, column)
|
41
|
+
string = string.to_s unless string.is_a? String
|
42
|
+
padding_length = max_line_length(column) - string.display_width
|
43
|
+
" #{string} " + (" " * padding_length)
|
44
|
+
end
|
45
|
+
|
46
|
+
def max_line_length(column)
|
47
|
+
@max[column] ||= ([@input_labels[column].to_s.length] + @test_data.map{|data| data[column].to_s.length}).max
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
%w[cucumber csv].each {|file| require "pairwise/formatter/#{file}"}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Pairwise
|
2
|
+
class InputData
|
3
|
+
|
4
|
+
def initialize(inputs)
|
5
|
+
@inputs = inputs.is_a?(Hash) ? hash_inputs_to_list(inputs) : inputs
|
6
|
+
end
|
7
|
+
|
8
|
+
def data
|
9
|
+
@data ||= @inputs.map {|input| input.values[0]}
|
10
|
+
end
|
11
|
+
|
12
|
+
def labels
|
13
|
+
@labels ||= @inputs.map{|input| input.keys}.flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def hash_inputs_to_list(inputs_hash)
|
19
|
+
inputs_hash.sort.map do |key, value|
|
20
|
+
value = [value] unless value.is_a?(Array)
|
21
|
+
{key => value}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Pairwise
|
2
|
+
class InputFile
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def load(filename)
|
6
|
+
inputs = self.new(filename).load_and_parse
|
7
|
+
InputData.new(inputs) if valid?(inputs)
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?(inputs)
|
11
|
+
inputs && (inputs.is_a?(Array) || inputs.is_a?(Hash))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(filename)
|
16
|
+
@filename = filename
|
17
|
+
self.extend(input_file_module)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def input_file_module
|
23
|
+
type = @filename[/\.(.+)$/, 1]
|
24
|
+
raise "Cannot determine file type for: #{@filename}" unless type
|
25
|
+
case type.downcase
|
26
|
+
when 'yaml', 'yml' then Yaml
|
27
|
+
else
|
28
|
+
Pairwise.const_get(type.capitalize) rescue raise "Unsupported file type: #{type}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
module Yaml
|
35
|
+
def load_and_parse
|
36
|
+
require 'yaml'
|
37
|
+
begin
|
38
|
+
inputs = YAML.load_file(@filename)
|
39
|
+
rescue
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module Csv
|
46
|
+
def load_and_parse
|
47
|
+
require 'csv'
|
48
|
+
|
49
|
+
csv_data = CSV.read @filename
|
50
|
+
headers = csv_data.shift.map {|head| head.to_s.strip }
|
51
|
+
string_data = csv_data.map {|row| row.map {|cell| cell.to_s.strip } }
|
52
|
+
|
53
|
+
inputs = Hash.new {|h,k| h[k] = []}
|
54
|
+
|
55
|
+
string_data.each do |row|
|
56
|
+
row.each_with_index { |value, index| inputs[headers[index]] << value }
|
57
|
+
end
|
58
|
+
|
59
|
+
inputs
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pairwise
|
2
|
+
class IPO
|
3
|
+
|
4
|
+
class Horizontal
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def growth(input_combinations, input_values_for_growth, previously_grown_input_values)
|
8
|
+
uncovered_pairs = PairCollection.new(input_values_for_growth, previously_grown_input_values, previously_grown_input_values.size)
|
9
|
+
input_combinations, uncovered_pairs = grow_input_combinations_and_remove_covered_pairs(input_combinations, input_values_for_growth, uncovered_pairs)
|
10
|
+
[input_combinations, uncovered_pairs]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def grow_input_combinations_and_remove_covered_pairs(input_combinations, input_values_for_growth, uncovered_pairs)
|
16
|
+
input_combinations = input_combinations.enum_for(:each_with_index).map do |input_combination, input_index|
|
17
|
+
extended_input_combination = uncovered_pairs.input_combination_that_covers_most_pairs(input_combination, input_values_for_growth)
|
18
|
+
uncovered_pairs.remove_pairs_covered_by!(extended_input_combination)
|
19
|
+
extended_input_combination
|
20
|
+
end
|
21
|
+
[input_combinations, uncovered_pairs]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Pairwise
|
2
|
+
class IPO
|
3
|
+
class Vertical
|
4
|
+
|
5
|
+
def self.growth(input_combinations, uncovered_pairs)
|
6
|
+
new_input_combinations = uncovered_pairs.reduce([]) do |new_input_combinations, uncovered_pair|
|
7
|
+
new_input_combinations = uncovered_pair.replace_wild_card(new_input_combinations)
|
8
|
+
end
|
9
|
+
|
10
|
+
input_combinations + new_input_combinations
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/pairwise/ipo.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# A pairwise implementation using the in-parameter-order (IPO) strategy.
|
2
|
+
# Based on: http://ranger.uta.edu/~ylei/paper/ipo-tse.pdf
|
3
|
+
module Pairwise
|
4
|
+
class IPO
|
5
|
+
|
6
|
+
WILD_CARD = 'wild_card'
|
7
|
+
|
8
|
+
def initialize(inputs, options = {})
|
9
|
+
@list_of_input_values = inputs
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
input_combinations = PairCollection.new(@list_of_input_values[0], [@list_of_input_values[1]], 0)
|
15
|
+
@list_of_input_values.size > 2 ? in_parameter_order_generation(input_combinations) : input_combinations.to_a
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def in_parameter_order_generation(input_combinations)
|
21
|
+
@list_of_input_values[2..-1].each_with_index do |input_values, index|
|
22
|
+
index += 2
|
23
|
+
previously_grown_input_values = @list_of_input_values[0..(index-1)]
|
24
|
+
|
25
|
+
input_combinations, uncovered_pairs = IPO::Horizontal.growth(input_combinations, input_values, previously_grown_input_values)
|
26
|
+
input_combinations = IPO::Vertical.growth(input_combinations, uncovered_pairs)
|
27
|
+
end
|
28
|
+
input_combinations = replace_wild_cards(input_combinations) unless @options[:keep_wild_cards]
|
29
|
+
input_combinations
|
30
|
+
end
|
31
|
+
|
32
|
+
def replace_wild_cards(input_combinations)
|
33
|
+
map_wild_cards(input_combinations) do |_, index|
|
34
|
+
if @list_of_input_values[index].length == 1
|
35
|
+
@list_of_input_values[index][0]
|
36
|
+
else
|
37
|
+
pick_random_value(@list_of_input_values[index])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def map_wild_cards(input_combinations)
|
43
|
+
input_combinations.map do |input_combination|
|
44
|
+
input_combination.enum_for(:each_with_index).map do |input_value, index|
|
45
|
+
input_value == WILD_CARD ? yield(index, index) : input_value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def map_each_input_value(input_combinations, &block)
|
51
|
+
input_combinations.map do |input_combination|
|
52
|
+
input_combination.enum_for(:each_with_index).map do |input_value, index|
|
53
|
+
yield input_value, index
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def pick_random_value(input_combination)
|
59
|
+
input_combination[Kernel.rand(input_combination.size)]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Pairwise
|
2
|
+
class PairCollection < Array
|
3
|
+
|
4
|
+
def initialize(input_parameter_values, input_value_lists, input_parameter_index)
|
5
|
+
pairs = generate_pairs_between(input_parameter_values, input_value_lists, input_parameter_index)
|
6
|
+
@combination_history = []
|
7
|
+
super(pairs)
|
8
|
+
end
|
9
|
+
|
10
|
+
def remove_pairs_covered_by!(extended_input_list)
|
11
|
+
self.reject!{|pair| pair.covered_by?(extended_input_list)}
|
12
|
+
end
|
13
|
+
|
14
|
+
def input_combination_that_covers_most_pairs(input_combination, input_values_for_growth)
|
15
|
+
candidates = input_values_for_growth.map{|value| input_combination + [value]}
|
16
|
+
max_combination = candidates.max {|combination_1, combination_2| pairs_covered_count(combination_1) <=> pairs_covered_count(combination_2)}
|
17
|
+
possible_max_combinations = candidates.select{|combination| pairs_covered_count(max_combination) == pairs_covered_count(combination)}
|
18
|
+
|
19
|
+
winner = if possible_max_combinations.size > 1 && !@combination_history.empty?
|
20
|
+
find_most_different_combination(possible_max_combinations)
|
21
|
+
else
|
22
|
+
possible_max_combinations[0]
|
23
|
+
end
|
24
|
+
@combination_history << winner
|
25
|
+
winner
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_a
|
29
|
+
self.map{|list| list.to_a}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def generate_pairs_between(input_parameter_values, input_value_lists, input_parameter_index)
|
35
|
+
pairs = []
|
36
|
+
input_parameter_values.each do |input_value_a|
|
37
|
+
input_value_lists.each_with_index do |input_list, input_value_b_index|
|
38
|
+
input_list.each do |input_value_b|
|
39
|
+
pairs << TestPair.new(input_parameter_index, input_value_b_index, input_value_a, input_value_b)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
pairs
|
44
|
+
end
|
45
|
+
|
46
|
+
def pairs_covered_count(input_combination)
|
47
|
+
self.reduce(0) do |covered_count, pair|
|
48
|
+
covered_count += 1 if pair.covered_by?(input_combination)
|
49
|
+
covered_count
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_most_different_combination(options)
|
54
|
+
scores = options.map do |option|
|
55
|
+
score(option)
|
56
|
+
end.flatten
|
57
|
+
|
58
|
+
_, winner_index = *scores.each_with_index.max
|
59
|
+
options[winner_index]
|
60
|
+
end
|
61
|
+
|
62
|
+
def score(option)
|
63
|
+
#O(n^2)
|
64
|
+
@combination_history.map do |previous_combination|
|
65
|
+
option.each_with_index.inject(0) do |difference_score, (value, index)|
|
66
|
+
value != previous_combination[index] ? difference_score : difference_score += 1
|
67
|
+
end
|
68
|
+
end.max
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
class TestPair
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegators :@pair, :+, :inspect, :to_a, :==
|
8
|
+
|
9
|
+
def initialize(p1_position, p2_position, p1, p2)
|
10
|
+
@p1, @p2 = p1, p2
|
11
|
+
@p1_position, @p2_position = p1_position, p2_position
|
12
|
+
@pair = [@p1, @p2]
|
13
|
+
end
|
14
|
+
|
15
|
+
def covered_by?(test_pair)
|
16
|
+
debugger unless test_pair
|
17
|
+
test_pair[@p1_position] == @p1 &&
|
18
|
+
test_pair[@p2_position] == @p2
|
19
|
+
end
|
20
|
+
|
21
|
+
def replace_wild_card(new_input_combinations)
|
22
|
+
#TODO: Decided if we should replace all matches or single matches?
|
23
|
+
if wild_card_index = find_wild_card_index(new_input_combinations)
|
24
|
+
new_input_combinations[wild_card_index][@p2_position] = @p2
|
25
|
+
else
|
26
|
+
new_input_combinations << create_input_list
|
27
|
+
end
|
28
|
+
new_input_combinations
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def create_input_list
|
33
|
+
new_input_list = Array.new(@p1_position){IPO::WILD_CARD}
|
34
|
+
|
35
|
+
new_input_list[@p1_position] = @p1
|
36
|
+
new_input_list[@p2_position] = @p2
|
37
|
+
new_input_list
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_wild_card_index(input_combinations)
|
41
|
+
wild_card_list = input_combinations.map do |input_combination|
|
42
|
+
input_combination[@p2_position] == IPO::WILD_CARD && input_combination[@p1_position] == @p1
|
43
|
+
end
|
44
|
+
wild_card_list.rindex(true)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/pairwise.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'pairwise/test_pair'
|
5
|
+
require 'pairwise/pair_collection'
|
6
|
+
require 'pairwise/ipo'
|
7
|
+
require 'pairwise/ipo/horizontal'
|
8
|
+
require 'pairwise/ipo/vertical'
|
9
|
+
require 'pairwise/formatter'
|
10
|
+
require 'pairwise/input_data'
|
11
|
+
require 'pairwise/input_file'
|
12
|
+
require 'pairwise/cli'
|
13
|
+
|
14
|
+
require 'yaml'
|
15
|
+
|
16
|
+
|
17
|
+
module Pairwise
|
18
|
+
class InvalidInputData < Exception; end
|
19
|
+
|
20
|
+
VERSION = '0.2.2'
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def combinations(*inputs)
|
24
|
+
raise InvalidInputData, "Minimum of 2 inputs are required to generate pairwise test set" unless valid?(inputs)
|
25
|
+
Pairwise::IPO.new(inputs).build
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def valid?(inputs)
|
30
|
+
array_of_arrays?(inputs) &&
|
31
|
+
inputs.length >= 2 &&
|
32
|
+
!inputs[0].empty? && !inputs[1].empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def array_of_arrays?(data)
|
36
|
+
data.reject{|datum| datum.kind_of?(Array)}.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/version.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
describe Cli do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Kernel.stub!(:exit).and_return(nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
def output_stream
|
11
|
+
@output_stream ||= StringIO.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def options
|
15
|
+
#TODO: push options out to an object and avoid hacking at private instance vars
|
16
|
+
@cli.instance_variable_get("@options")
|
17
|
+
end
|
18
|
+
|
19
|
+
def prepare_args(args)
|
20
|
+
args.is_a?(Array) ? args : args.split(' ')
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_parsing(args)
|
24
|
+
@cli = Pairwise::Cli.new(prepare_args(args), output_stream)
|
25
|
+
@cli.parse!
|
26
|
+
yield
|
27
|
+
end
|
28
|
+
|
29
|
+
context '--keep-wild-cards' do
|
30
|
+
it "displays wild cards in output result" do
|
31
|
+
after_parsing('--keep-wild-cards') do
|
32
|
+
options[:keep_wild_cards].should == true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context '-f FORMAT or --format FORMAT' do
|
38
|
+
it "defaults to the cucumber format for output" do
|
39
|
+
after_parsing('') do
|
40
|
+
options[:format].should == 'cucumber'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "overides the cucumber format when passed a specific format" do
|
45
|
+
after_parsing('--format csv') do
|
46
|
+
options[:format].should == 'csv'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "--help" do
|
52
|
+
it "displays usage" do
|
53
|
+
after_parsing('--help') do
|
54
|
+
output_stream.string.should =~ /Usage: pairwise \[options\] FILE\.\[yml|csv\]/
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context '--version' do
|
60
|
+
it "displays Pairwise's version" do
|
61
|
+
after_parsing('--version') do
|
62
|
+
output_stream.string.should =~ /#{Pairwise::VERSION}/
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
class IPO
|
5
|
+
|
6
|
+
describe Horizontal do
|
7
|
+
describe ".growth" do
|
8
|
+
context "when the input_combinations size is smaller than the input values for growth size" do
|
9
|
+
it "should extenhd with C's inputs" do
|
10
|
+
input_combinations = [[:A1, :B1], [:A1, :B2]]
|
11
|
+
data = [[:A1, :A2], [:C1, :C2, :C3 ]]
|
12
|
+
|
13
|
+
test_set, _ = Horizontal.growth(input_combinations, data[1], data[0..1])
|
14
|
+
|
15
|
+
test_set.should == [[:A1, :B1, :C1], [:A1, :B2, :C3]]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when the input_combinations size is larger than the input values for growth size" do
|
20
|
+
before(:each) do
|
21
|
+
@test_pairs = [[:A1, :B1], [:A1, :B2], [:A2, :B1], [:A2, :B2]]
|
22
|
+
@data = [[:A1, :A2], [:B1, :B2], [:C1, :C2, :C3 ]]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return pairs extended with C's inputs" do
|
26
|
+
test_set, _ = Horizontal.growth(@test_pairs, @data[2], @data[0..1])
|
27
|
+
|
28
|
+
test_set.should == [[:A1, :B1, :C1], [:A1, :B2, :C3], [:A2, :B1, :C3], [:A2, :B2, :C2]]
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return all the uncovered pairs" do
|
32
|
+
_, pi = Horizontal.growth(@test_pairs, @data[2], @data[0..1])
|
33
|
+
|
34
|
+
pi.to_a.should == [[:C1, :A2], [:C1, :B2], [:C2, :A1], [:C2, :B1]]
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
describe PairCollection do
|
5
|
+
describe '#input_combination_that_covers_most_pairs' do
|
6
|
+
it "should do find the combination that covers most pairs" do
|
7
|
+
pair_collection = PairCollection.new([:A2, :B2], [[:A2, :B2], [:B2, :A1]], 1)
|
8
|
+
|
9
|
+
combination = pair_collection.input_combination_that_covers_most_pairs([:A2, :B2], [:C2, :A1])
|
10
|
+
|
11
|
+
combination.should == [:A2, :B2, :C2]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Pairwise do
|
4
|
+
before(:each) do
|
5
|
+
Kernel.stub!(:rand).and_return(0)
|
6
|
+
end
|
7
|
+
|
8
|
+
context "with invalid inputs" do
|
9
|
+
it "should be invalid when running with no input" do
|
10
|
+
lambda{ Pairwise.combinations([]) }.should raise_error(Pairwise::InvalidInputData)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be invalid when running with only 1 input" do
|
14
|
+
lambda{ Pairwise.combinations([:A1, :A2])}.should raise_error(Pairwise::InvalidInputData)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be invalid when running with a single list input" do
|
18
|
+
lambda{ Pairwise.combinations([:A1, :A2])}.should raise_error(Pairwise::InvalidInputData)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with valid inputs" do
|
23
|
+
context "which are equal lengths" do
|
24
|
+
it "should generate pairs for 2 parameters of 1 value" do
|
25
|
+
data = [[:A1], [:B1]]
|
26
|
+
|
27
|
+
Pairwise.combinations(*data).should == [[:A1, :B1]]
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should generate all pairs for 2 parameters of 2 values" do
|
31
|
+
data = [[:A1, :A2], [:B1, :B2]]
|
32
|
+
|
33
|
+
Pairwise.combinations(*data).should == [[:A1, :B1], [:A1, :B2], [:A2, :B1], [:A2, :B2]]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should generate all pairs for 3 parameters of 1 value" do
|
37
|
+
data = [[:A1], [:B1], [:C1]]
|
38
|
+
|
39
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1]]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should generate pairs for three paramters" do
|
43
|
+
data = [[:A1, :A2],
|
44
|
+
[:B1, :B2],
|
45
|
+
[:C1 , :C2]]
|
46
|
+
|
47
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1],
|
48
|
+
[:A1, :B2, :C2],
|
49
|
+
[:A2, :B1, :C2],
|
50
|
+
[:A2, :B2, :C1]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "which are unequal lengths" do
|
55
|
+
it "should generate all pairs for 3 parameters of 1,1,2 values" do
|
56
|
+
data = [[:A1], [:B1], [:C1, :C2]]
|
57
|
+
|
58
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1],
|
59
|
+
[:A1, :B1, :C2]]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should generate all pairs for 3 parameters of 1,1,3 values" do
|
63
|
+
data = [[:A1], [:B1], [:C1, :C2, :C3]]
|
64
|
+
|
65
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1],
|
66
|
+
[:A1, :B1, :C2],
|
67
|
+
[:A1, :B1, :C3]]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should generate all pairs for 3 parameters of 1,2,3 values" do
|
71
|
+
data = [[:A1], [:B1, :B2], [:C1, :C2, :C3]]
|
72
|
+
|
73
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1], [:A1, :B2, :C3], [:A1, :B2, :C1], [:A1, :B1, :C2], [:A1, :B2, :C2], [:A1, :B1, :C3]]
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should generate all pairs for 3 parameters of 2,1,2 values" do
|
77
|
+
data = [[:A1, :A2], [:B1], [:C1, :C2]]
|
78
|
+
|
79
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1],
|
80
|
+
[:A2, :B1, :C2],
|
81
|
+
[:A2, :B1, :C1],
|
82
|
+
[:A1, :B1, :C2]]
|
83
|
+
|
84
|
+
|
85
|
+
#:A1, :B1, :C1
|
86
|
+
#:A1, :B1, :C2
|
87
|
+
#:A2, :B1, :C1
|
88
|
+
#:A2,any_value_of_B, :C2
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should generate all pairs for 4 parameters of 2,1,2,2 values" do
|
92
|
+
data = [[:A1, :A2], [:B1], [:C1, :C2], [:D1, :D2]]
|
93
|
+
|
94
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1, :D1], [:A2, :B1, :C2, :D2], [:A2, :B1, :C1, :D2], [:A1, :B1, :C2, :D2], [:A2, :B1, :C2, :D1]]
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should generate pairs for three parameters" do
|
98
|
+
data = [[:A1, :A2],
|
99
|
+
[:B1, :B2],
|
100
|
+
[:C1 , :C2 , :C3 ]]
|
101
|
+
|
102
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1], [:A1, :B2, :C3], [:A2, :B1, :C3], [:A2, :B2, :C2], [:A2, :B2, :C1], [:A1, :B1, :C2]]
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "replacing wildcards which could have more than one option" do
|
106
|
+
it "should generate pairs for 2 parameters of 3,2,3 values" do
|
107
|
+
Pairwise.combinations([:A1, :A2, :A3],
|
108
|
+
[:B1, :B2],
|
109
|
+
[:C1, :C2, :C3]).should == [[:A1, :B1, :C1],
|
110
|
+
[:A1, :B2, :C3],
|
111
|
+
[:A2, :B1, :C3],
|
112
|
+
[:A2, :B2, :C2],
|
113
|
+
[:A3, :B1, :C2],
|
114
|
+
[:A3, :B2, :C1],
|
115
|
+
[:A2, :B1, :C1],
|
116
|
+
[:A1, :B1, :C2],
|
117
|
+
[:A3, :B1, :C3]]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pairwise_psych
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joseph Wilk
|
8
|
+
- Ali King
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-06-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: unicode-display_width
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: cucumber
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
description: |-
|
57
|
+
A tool for selecting a smaller number of test combinations (using pairwise generation) rather than exhaustively testing all possible permutations.
|
58
|
+
To read about why, go here: http://www.pairwise.org/
|
59
|
+
This variation uses the newer psych yaml engine
|
60
|
+
email: ali@animoto.com
|
61
|
+
executables:
|
62
|
+
- pairwise
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- README.md
|
67
|
+
- bin/pairwise
|
68
|
+
- lib/pairwise.rb
|
69
|
+
- lib/pairwise/cli.rb
|
70
|
+
- lib/pairwise/formatter.rb
|
71
|
+
- lib/pairwise/formatter/csv.rb
|
72
|
+
- lib/pairwise/formatter/cucumber.rb
|
73
|
+
- lib/pairwise/input_data.rb
|
74
|
+
- lib/pairwise/input_file.rb
|
75
|
+
- lib/pairwise/ipo.rb
|
76
|
+
- lib/pairwise/ipo/horizontal.rb
|
77
|
+
- lib/pairwise/ipo/vertical.rb
|
78
|
+
- lib/pairwise/pair_collection.rb
|
79
|
+
- lib/pairwise/test_pair.rb
|
80
|
+
- lib/version.rb
|
81
|
+
- spec/pairwise/cli_spec.rb
|
82
|
+
- spec/pairwise/ipo/horizontal_spec.rb
|
83
|
+
- spec/pairwise/ipo_spec.rb
|
84
|
+
- spec/pairwise/pair_collection_spec.rb
|
85
|
+
- spec/pairwise_spec.rb
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
homepage: https://aliking.github.io/pairwise/
|
88
|
+
licenses: []
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.5.2
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Turn large test data combinations into smaller ones
|
110
|
+
test_files: []
|
111
|
+
has_rdoc:
|