pairwise 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/LICENSE +22 -0
- data/Rakefile +25 -0
- data/VERSION.yml +4 -0
- data/bin/pairwise +7 -0
- data/features/generating_pairwise_data.feature +80 -0
- data/features/step_definitions/pairwise_data_steps.rb +15 -0
- data/features/support/env.rb +76 -0
- data/features/support/hooks.rb +4 -0
- data/gem_tasks/features.rake +10 -0
- data/gem_tasks/rspec.rake +15 -0
- data/lib/pairwise.rb +35 -0
- data/lib/pairwise/builder.rb +142 -0
- data/lib/pairwise/cli.rb +78 -0
- data/lib/pairwise/formatter.rb +1 -0
- data/lib/pairwise/formatter/cucumber.rb +40 -0
- data/lib/pairwise/test_pair.rb +41 -0
- data/spec/pairwise/builder_spec.rb +33 -0
- data/spec/pairwise/cli_spec.rb +54 -0
- data/spec/pairwise_spec.rb +125 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +4 -0
- metadata +98 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2009 Joseph Wilk
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "pairwise"
|
8
|
+
gem.summary = %Q{Turn large test data combinations into smaller ones}
|
9
|
+
gem.description = %Q{A tool for selecting a smaller number of test combinations (using pairwise generation) rather than exhaustively testing all possible permutations.}
|
10
|
+
gem.email = "joe@josephwilk.net"
|
11
|
+
gem.homepage = "http://wiki.github.com/josephwilk/pairwise"
|
12
|
+
gem.authors = ["Joseph Wilk"]
|
13
|
+
|
14
|
+
gem.add_development_dependency 'rspec'
|
15
|
+
gem.add_development_dependency 'cucumber'
|
16
|
+
end
|
17
|
+
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
Dir['gem_tasks/**/*.rake'].each { |rake| load rake }
|
24
|
+
|
25
|
+
task :default => ["spec", "features"]
|
data/VERSION.yml
ADDED
data/bin/pairwise
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
Feature: Generating pairwise data
|
2
|
+
In order to test small, managable datasets while having confidence in test coverage
|
3
|
+
As a tester
|
4
|
+
I want a set of tests which is smaller than all the possible combinations of my specified inputs
|
5
|
+
|
6
|
+
Scenario: No input file specified
|
7
|
+
When I run pairwise
|
8
|
+
Then I should see in the output
|
9
|
+
"""
|
10
|
+
Usage: pairwise [options] FILE.yml
|
11
|
+
"""
|
12
|
+
|
13
|
+
Scenario: Ordered yaml inputs
|
14
|
+
Given I have the yaml file "inputs.yml" containing:
|
15
|
+
"""
|
16
|
+
- event with image: [Football, Basketball, Soccer]
|
17
|
+
- event without image: [Football, Basketball, Soccer]
|
18
|
+
- media: [Image, Video, Music]
|
19
|
+
"""
|
20
|
+
When I run pairwise inputs.yml
|
21
|
+
Then I should see the output
|
22
|
+
"""
|
23
|
+
| event with image | event without image | media |
|
24
|
+
| Football | Football | Image |
|
25
|
+
| Football | Basketball | Video |
|
26
|
+
| Football | Soccer | Music |
|
27
|
+
| Basketball | Football | Music |
|
28
|
+
| Basketball | Basketball | Image |
|
29
|
+
| Basketball | Soccer | Video |
|
30
|
+
| Soccer | Football | Video |
|
31
|
+
| Soccer | Basketball | Music |
|
32
|
+
| Soccer | Soccer | Image |
|
33
|
+
|
34
|
+
"""
|
35
|
+
|
36
|
+
Scenario: Unorderd yaml inputs
|
37
|
+
Given I have the yaml file "inputs.yml" containing:
|
38
|
+
"""
|
39
|
+
event with image: [Football, Basketball, Soccer]
|
40
|
+
event without image: [Football, Basketball, Soccer]
|
41
|
+
media: [Image, Video, Music]
|
42
|
+
"""
|
43
|
+
When I run pairwise inputs.yml
|
44
|
+
Then I should see the output
|
45
|
+
"""
|
46
|
+
| media | event without image | event with image |
|
47
|
+
| Image | Football | Football |
|
48
|
+
| Image | Basketball | Basketball |
|
49
|
+
| Image | Soccer | Soccer |
|
50
|
+
| Video | Football | Soccer |
|
51
|
+
| Video | Basketball | Football |
|
52
|
+
| Video | Soccer | Basketball |
|
53
|
+
| Music | Football | Basketball |
|
54
|
+
| Music | Basketball | Soccer |
|
55
|
+
| Music | Soccer | Football |
|
56
|
+
|
57
|
+
"""
|
58
|
+
|
59
|
+
Scenario: Not replacing wild cards
|
60
|
+
Given I have the yaml file "inputs.yml" containing:
|
61
|
+
"""
|
62
|
+
- A: [A1, A2, A3]
|
63
|
+
- B: [B1, B2]
|
64
|
+
- C: [C1, C2, C3]
|
65
|
+
"""
|
66
|
+
When I run pairwise inputs.yml --keep-wild-cards
|
67
|
+
Then I should see the output
|
68
|
+
"""
|
69
|
+
| A | B | C |
|
70
|
+
| A1 | B1 | C1 |
|
71
|
+
| A1 | B2 | C2 |
|
72
|
+
| A2 | B1 | C3 |
|
73
|
+
| A2 | B2 | C1 |
|
74
|
+
| A3 | B1 | C2 |
|
75
|
+
| A3 | B2 | C3 |
|
76
|
+
| A3 | wild_card | C1 |
|
77
|
+
| A2 | wild_card | C2 |
|
78
|
+
| A1 | wild_card | C3 |
|
79
|
+
|
80
|
+
"""
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Given /^I have the yaml file "([^\"]*)" containing:$/ do |file_name, file_contents|
|
2
|
+
create_file(file_name, file_contents)
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I run (.+)$/ do |command|
|
6
|
+
run(command)
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^I should see the output$/ do |text|
|
10
|
+
last_stdout.should == text
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^I should see in the output$/ do |string|
|
14
|
+
last_stdout.should include(string)
|
15
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'spec/expectations'
|
4
|
+
require "spec/mocks"
|
5
|
+
require 'fileutils'
|
6
|
+
require 'forwardable'
|
7
|
+
|
8
|
+
SCRATCH_SPACE = 'tmp'
|
9
|
+
|
10
|
+
class PairwiseWorld
|
11
|
+
extend Forwardable
|
12
|
+
def_delegators PairwiseWorld, :self_test_dir
|
13
|
+
|
14
|
+
def self.examples_dir(subdir=nil)
|
15
|
+
@examples_dir ||= File.expand_path(File.join(File.dirname(__FILE__), "../../#{SCRATCH_SPACE}"))
|
16
|
+
subdir ? File.join(@examples_dir, subdir) : @examples_dir
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.self_test_dir
|
20
|
+
@self_test_dir ||= examples_dir
|
21
|
+
end
|
22
|
+
|
23
|
+
def pairwise_lib_dir
|
24
|
+
@pairwise_lib_dir ||= File.expand_path(File.join(File.dirname(__FILE__), '../../lib'))
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@current_dir = self_test_dir
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
attr_reader :last_exit_status, :last_stderr
|
33
|
+
|
34
|
+
def last_stdout
|
35
|
+
@last_stdout
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_file(file_name, file_content)
|
39
|
+
in_current_dir do
|
40
|
+
FileUtils.mkdir_p(File.dirname(file_name)) unless File.directory?(File.dirname(file_name))
|
41
|
+
File.open(file_name, 'w') { |f| f << file_content }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_env_var(variable, value)
|
46
|
+
@original_env_vars ||= {}
|
47
|
+
@original_env_vars[variable] = ENV[variable]
|
48
|
+
ENV[variable] = value
|
49
|
+
end
|
50
|
+
|
51
|
+
def in_current_dir(&block)
|
52
|
+
Dir.chdir(@current_dir, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def run(command)
|
56
|
+
stderr_file = Tempfile.new('pairwise')
|
57
|
+
stderr_file.close
|
58
|
+
in_current_dir do
|
59
|
+
IO.popen("../bin/#{command} 2> #{stderr_file.path}", 'r') do |io|
|
60
|
+
@last_stdout = io.read
|
61
|
+
end
|
62
|
+
|
63
|
+
@last_exit_status = $?.exitstatus
|
64
|
+
end
|
65
|
+
@last_stderr = IO.read(stderr_file.path)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
World do
|
70
|
+
PairwiseWorld.new
|
71
|
+
end
|
72
|
+
|
73
|
+
Before do
|
74
|
+
FileUtils.rm_rf SCRATCH_SPACE
|
75
|
+
FileUtils.mkdir SCRATCH_SPACE
|
76
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec/expectations'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
desc "Run RSpec"
|
6
|
+
Spec::Rake::SpecTask.new do |t|
|
7
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
8
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
9
|
+
t.rcov = ENV['RCOV']
|
10
|
+
t.rcov_opts = %w{--exclude gems\/,spec\/}
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
task :spec
|
15
|
+
end
|
data/lib/pairwise.rb
ADDED
@@ -0,0 +1,35 @@
|
|
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/builder'
|
6
|
+
require 'pairwise/formatter'
|
7
|
+
require 'pairwise/cli'
|
8
|
+
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
module Pairwise
|
12
|
+
class InvalidInputData < Exception; end
|
13
|
+
|
14
|
+
version = YAML.load_file(File.dirname(__FILE__) + '/../VERSION.yml')
|
15
|
+
VERSION = [version[:major], version[:minor], version[:patch], version[:build]].compact.join('.')
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def combinations(*inputs)
|
19
|
+
raise InvalidInputData, "Minimum of 2 inputs are required to generate pairwise test set" unless valid?(inputs)
|
20
|
+
Pairwise::Builder.new(inputs).build
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def valid?(inputs)
|
25
|
+
array_of_arrays?(inputs) &&
|
26
|
+
inputs.length >= 2 &&
|
27
|
+
!inputs[0].empty? && !inputs[1].empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def array_of_arrays?(data)
|
31
|
+
data.reject{|datum| datum.kind_of?(Array)}.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,142 @@
|
|
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 Builder
|
5
|
+
|
6
|
+
WILD_CARD = 'wild_card'
|
7
|
+
|
8
|
+
def initialize(inputs, options = {})
|
9
|
+
@inputs = inputs
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
input_lists = generate_pairs_between(@inputs[0], [@inputs[1]], 0)
|
15
|
+
@inputs.size > 2 ? in_parameter_order_generation(input_lists) : input_lists.map{|list| list.to_a}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def in_parameter_order_generation(input_lists)
|
21
|
+
@inputs[2..-1].each_with_index do |input_list, i|
|
22
|
+
i += 2
|
23
|
+
input_lists, pi = horizontal_growth(input_lists, input_list, @inputs[0..(i-1)])
|
24
|
+
input_lists = vertical_growth(input_lists, pi)
|
25
|
+
end
|
26
|
+
input_lists = replace_redundant_wild_cards(input_lists)
|
27
|
+
input_lists = replace_wild_cards(input_lists) unless @options[:keep_wild_cards]
|
28
|
+
input_lists
|
29
|
+
end
|
30
|
+
|
31
|
+
def horizontal_growth(input_lists, parameter_i, inputs)
|
32
|
+
pi = generate_pairs_between(parameter_i, inputs, inputs.size)
|
33
|
+
|
34
|
+
if input_lists.size <= parameter_i.size
|
35
|
+
input_lists = input_lists.enum_for(:each_with_index).map do |input_list, index_of_input_under_inspection|
|
36
|
+
extended_input_list = input_list + [parameter_i[index_of_input_under_inspection]]
|
37
|
+
pi = remove_pairs_covered_by(extended_input_list, pi)
|
38
|
+
extended_input_list
|
39
|
+
end
|
40
|
+
else
|
41
|
+
input_lists[0...parameter_i.size] = input_lists[0...parameter_i.size].enum_for(:each_with_index).map do |input_list, index_of_input_under_inspection|
|
42
|
+
extended_input_list = input_list + [parameter_i[index_of_input_under_inspection]]
|
43
|
+
pi = remove_pairs_covered_by(extended_input_list, pi)
|
44
|
+
extended_input_list
|
45
|
+
end
|
46
|
+
|
47
|
+
input_lists[parameter_i.size..-1] = input_lists[parameter_i.size..-1].map do |input_list|
|
48
|
+
extended_input_list = input_list_that_covers_most_pairs(input_list, parameter_i, pi)
|
49
|
+
pi = remove_pairs_covered_by(extended_input_list, pi)
|
50
|
+
extended_input_list
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
[input_lists, pi]
|
55
|
+
end
|
56
|
+
|
57
|
+
def vertical_growth(input_lists, pi)
|
58
|
+
new_input_lists = []
|
59
|
+
|
60
|
+
pi.each do |uncovered_pair|
|
61
|
+
#TODO: Decided if we should replace all matches or single matches?
|
62
|
+
if test_position = uncovered_pair.replaceable_wild_card?(new_input_lists)
|
63
|
+
new_input_lists[test_position] = uncovered_pair.replace_wild_card(new_input_lists[test_position])
|
64
|
+
else
|
65
|
+
new_input_lists << uncovered_pair.create_input_list
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
input_lists + new_input_lists
|
70
|
+
end
|
71
|
+
|
72
|
+
def replace_redundant_wild_cards(input_lists)
|
73
|
+
map_each_input_value(input_lists) do |input_value, index|
|
74
|
+
if input_value == WILD_CARD && @inputs[index].length == 1
|
75
|
+
@inputs[index][0]
|
76
|
+
else
|
77
|
+
input_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def replace_wild_cards(input_lists)
|
83
|
+
map_each_input_value(input_lists) do |input_value, index|
|
84
|
+
if input_value == WILD_CARD
|
85
|
+
pick_random_value(@inputs[index])
|
86
|
+
else
|
87
|
+
input_value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def map_each_input_value(input_lists)
|
93
|
+
input_lists.map do |input_list|
|
94
|
+
input_list.enum_for(:each_with_index).map do |input_value, index|
|
95
|
+
yield input_value, index
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def pick_random_value(input_list)
|
101
|
+
input_list[rand(input_list.size)]
|
102
|
+
end
|
103
|
+
|
104
|
+
def generate_pairs_between(parameter_i, input_lists, p_index)
|
105
|
+
pairs = []
|
106
|
+
parameter_i.each do |p|
|
107
|
+
input_lists.each_with_index do |input_list, q_index|
|
108
|
+
input_list.each do |q|
|
109
|
+
pairs << TestPair.new(p_index, q_index, p, q)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
pairs
|
114
|
+
end
|
115
|
+
|
116
|
+
def remove_pairs_covered_by(extended_input_list, pi)
|
117
|
+
pi.reject{|pair| pair.covered_by?(extended_input_list)}
|
118
|
+
end
|
119
|
+
|
120
|
+
def input_list_that_covers_most_pairs(input_list, parameter_i, pi)
|
121
|
+
selected_input_list = nil
|
122
|
+
parameter_i.reduce(0) do |max_covered_count, value|
|
123
|
+
input_list_candidate = input_list + [value]
|
124
|
+
covered_count = pairs_covered_count(input_list_candidate, pi)
|
125
|
+
if covered_count >= max_covered_count
|
126
|
+
selected_input_list = input_list_candidate
|
127
|
+
covered_count
|
128
|
+
else
|
129
|
+
max_covered_count
|
130
|
+
end
|
131
|
+
end
|
132
|
+
selected_input_list
|
133
|
+
end
|
134
|
+
|
135
|
+
def pairs_covered_count(input_list, pairs)
|
136
|
+
pairs.reduce(0) do |covered_count, pair|
|
137
|
+
covered_count += 1 if pair.covered_by?(input_list)
|
138
|
+
covered_count
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/pairwise/cli.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'optparse'
|
3
|
+
module Pairwise
|
4
|
+
class Cli
|
5
|
+
class << self
|
6
|
+
def execute(args)
|
7
|
+
new(args).execute!
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(args, out = STDOUT)
|
12
|
+
@args, @out = args, out
|
13
|
+
@formatter = Formatter::Cucumber.new(@out)
|
14
|
+
@options = defaults
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse!
|
18
|
+
@args.extend(::OptionParser::Arguable)
|
19
|
+
@args.options do |opts|
|
20
|
+
opts.banner = ["Usage: pairwise [options] FILE.yml", "",
|
21
|
+
"Example:",
|
22
|
+
"pairwise data/inputs.yml", "", "",
|
23
|
+
].join("\n")
|
24
|
+
opts.on("-k", "--keep-wild-cards") do
|
25
|
+
@options[:keep_wild_cards] = true
|
26
|
+
end
|
27
|
+
opts.on_tail("--version", "Show version.") do
|
28
|
+
@out.puts Pairwise::VERSION
|
29
|
+
Kernel.exit(0)
|
30
|
+
end
|
31
|
+
opts.on_tail("-h", "--help", "You're looking at it.") do
|
32
|
+
exit_with_help
|
33
|
+
end
|
34
|
+
end.parse!
|
35
|
+
|
36
|
+
@input_file = @args[0] unless @args.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute!
|
40
|
+
parse!
|
41
|
+
exit_with_help if @input_file.nil? || @input_file.empty?
|
42
|
+
input_data, input_labels = *load_and_parse_input_file!
|
43
|
+
|
44
|
+
builder = Pairwise::Builder.new(input_data, @options)
|
45
|
+
|
46
|
+
@formatter.display(builder.build, input_labels)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def defaults
|
51
|
+
{:keep_wild_cards => false}
|
52
|
+
end
|
53
|
+
|
54
|
+
def exit_with_help
|
55
|
+
@out.puts @args.options.help
|
56
|
+
Kernel.exit(0)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_and_parse_input_file!
|
60
|
+
inputs = YAML.load_file(@input_file)
|
61
|
+
inputs = hash_inputs_to_list(inputs) if inputs.is_a?(Hash)
|
62
|
+
|
63
|
+
raw_inputs = inputs.map {|input| input.values[0]}
|
64
|
+
input_labels = input_names(inputs)
|
65
|
+
|
66
|
+
[raw_inputs, input_labels]
|
67
|
+
end
|
68
|
+
|
69
|
+
def hash_inputs_to_list(inputs_hash)
|
70
|
+
inputs_hash.map {|key, value| {key => value}}
|
71
|
+
end
|
72
|
+
|
73
|
+
def input_names(inputs)
|
74
|
+
inputs.map{|input| input.keys}.flatten
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
%q[cucumber].each {|file| require "pairwise/formatter/#{file}"}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Pairwise
|
2
|
+
module Formatter
|
3
|
+
class Cucumber
|
4
|
+
|
5
|
+
def initialize(out)
|
6
|
+
@out = out
|
7
|
+
@max = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def display(test_data, inputs)
|
11
|
+
@test_data, @inputs = test_data, inputs
|
12
|
+
|
13
|
+
@out.print "|"
|
14
|
+
inputs.each_with_index do |key, column|
|
15
|
+
@out.print padded_string(key, column) + "|"
|
16
|
+
end
|
17
|
+
@out.puts
|
18
|
+
|
19
|
+
test_data.each do |data|
|
20
|
+
@out.print "|"
|
21
|
+
data.each_with_index do |datum, column|
|
22
|
+
@out.print padded_string(datum, column) + "|"
|
23
|
+
end
|
24
|
+
@out.puts
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def padded_string(string, column)
|
30
|
+
padding_length = max_line_length(column) - string.length
|
31
|
+
" #{string} " + (" " * padding_length)
|
32
|
+
end
|
33
|
+
|
34
|
+
def max_line_length(column)
|
35
|
+
@max[column] ||= ([@inputs[column].length] + @test_data.map{|data| data[column].length}).max
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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
|
+
test_pair[@p1_position] == @p1 &&
|
17
|
+
test_pair[@p2_position] == @p2
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_input_list
|
21
|
+
new_input_list = Array.new(@p1_position){Builder::WILD_CARD}
|
22
|
+
|
23
|
+
new_input_list[@p1_position] = @p1
|
24
|
+
new_input_list[@p2_position] = @p2
|
25
|
+
new_input_list
|
26
|
+
end
|
27
|
+
|
28
|
+
def replace_wild_card(input_list)
|
29
|
+
input_list[@p2_position] = @p2
|
30
|
+
input_list
|
31
|
+
end
|
32
|
+
|
33
|
+
def replaceable_wild_card?(input_lists)
|
34
|
+
wild_card_list = input_lists.map do |input_list|
|
35
|
+
input_list[@p2_position] == Builder::WILD_CARD && input_list[@p1_position] == @p1
|
36
|
+
end
|
37
|
+
wild_card_list.rindex(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
module Pairwise
|
4
|
+
describe Builder do
|
5
|
+
|
6
|
+
describe 'ipo horizontal growth' do
|
7
|
+
before(:each) do
|
8
|
+
@test_pairs = [[:A1, :B1], [:A1, :B2], [:A2, :B1], [:A2, :B2]]
|
9
|
+
@data = [[:A1, :A2],[:B1, :B2],[:C1 , :C2 , :C3 ]]
|
10
|
+
|
11
|
+
@builder = Builder.new(@test_pairs)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should return pairs extended with C's inputs" do
|
15
|
+
test_set, _ = @builder.send(:horizontal_growth, @test_pairs, @data[2], @data[0..1])
|
16
|
+
|
17
|
+
test_set.should == [[:A1, :B1, :C1],
|
18
|
+
[:A1, :B2, :C2],
|
19
|
+
[:A2, :B1, :C3],
|
20
|
+
[:A2, :B2, :C1]]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return all the uncovered pairs" do
|
24
|
+
_, pi = @builder.send(:horizontal_growth, @test_pairs, @data[2], @data[0..1])
|
25
|
+
|
26
|
+
# We are getting the uncovered pairs in reverse
|
27
|
+
#pi.should == [[:A2, :C2],[:A1, :C3],[:B1, :C2],[:B2, :C3]]
|
28
|
+
# Cheat and check we get the list in reverse
|
29
|
+
pi.should == [[:C2, :A2], [:C2, :B1], [:C3, :A1], [:C3, :B2]]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../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 "--help" do
|
38
|
+
it "displays usage" do
|
39
|
+
after_parsing('--help') do
|
40
|
+
output_stream.string.should =~ /Usage: pairwise \[options\] FILE\.yml/
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context '--version' do
|
46
|
+
it "displays Pairwise's version" do
|
47
|
+
after_parsing('--version') do
|
48
|
+
output_stream.string.should =~ /#{Pairwise::VERSION}/
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/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 lenghts" 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],
|
74
|
+
[:A1, :B2, :C2],
|
75
|
+
[:A1, :B2, :C1],
|
76
|
+
[:A1, :B1, :C2],
|
77
|
+
[:A1, :B1, :C3],
|
78
|
+
[:A1, :B2, :C3]]
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should generate all pairs for 3 parameters of 2,1,2 values" do
|
82
|
+
data = [[:A1, :A2], [:B1], [:C1, :C2]]
|
83
|
+
|
84
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1],
|
85
|
+
[:A2, :B1, :C2],
|
86
|
+
[:A2, :B1, :C1],
|
87
|
+
[:A1, :B1, :C2]]
|
88
|
+
|
89
|
+
|
90
|
+
#:A1, :B1, :C1
|
91
|
+
#:A1, :B1, :C2
|
92
|
+
#:A2, :B1, :C1
|
93
|
+
#:A2,any_value_of_B, :C2
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should generate all pairs for 4 parameters of 2,1,2,2 values" do
|
97
|
+
data = [[:A1, :A2], [:B1], [:C1, :C2], [:D1, :D2]]
|
98
|
+
|
99
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1, :D1],
|
100
|
+
[:A2, :B1, :C2, :D2],
|
101
|
+
[:A2, :B1, :C1, :D2],
|
102
|
+
[:A1, :B1, :C2, :D2],
|
103
|
+
[:A2, :B1, :C2, :D1]]
|
104
|
+
#:A1, :B1, :C1, :D1
|
105
|
+
#:A1, :B1, :C2, :D2
|
106
|
+
#:A2, any_value_of_B, :C2, :D1
|
107
|
+
#:A2, :B1, :C1, :D2
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should generate pairs for three paramters" do
|
111
|
+
data = [[:A1, :A2],
|
112
|
+
[:B1, :B2],
|
113
|
+
[:C1 , :C2 , :C3 ]]
|
114
|
+
|
115
|
+
Pairwise.combinations(*data).should == [[:A1, :B1, :C1],
|
116
|
+
[:A1, :B2, :C2],
|
117
|
+
[:A2, :B1, :C3],
|
118
|
+
[:A2, :B2, :C1],
|
119
|
+
[:A2, :B1, :C2],
|
120
|
+
[:A1, :B2, :C3]]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pairwise
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joseph Wilk
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-05 00:00:00 +00:00
|
13
|
+
default_executable: pairwise
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: cucumber
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: A tool for selecting a smaller number of test combinations (using pairwise generation) rather than exhaustively testing all possible permutations.
|
36
|
+
email: joe@josephwilk.net
|
37
|
+
executables:
|
38
|
+
- pairwise
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- LICENSE
|
46
|
+
- Rakefile
|
47
|
+
- VERSION.yml
|
48
|
+
- bin/pairwise
|
49
|
+
- features/generating_pairwise_data.feature
|
50
|
+
- features/step_definitions/pairwise_data_steps.rb
|
51
|
+
- features/support/env.rb
|
52
|
+
- features/support/hooks.rb
|
53
|
+
- gem_tasks/features.rake
|
54
|
+
- gem_tasks/rspec.rake
|
55
|
+
- lib/pairwise.rb
|
56
|
+
- lib/pairwise/builder.rb
|
57
|
+
- lib/pairwise/cli.rb
|
58
|
+
- lib/pairwise/formatter.rb
|
59
|
+
- lib/pairwise/formatter/cucumber.rb
|
60
|
+
- lib/pairwise/test_pair.rb
|
61
|
+
- spec/pairwise/builder_spec.rb
|
62
|
+
- spec/pairwise/cli_spec.rb
|
63
|
+
- spec/pairwise_spec.rb
|
64
|
+
- spec/spec.opts
|
65
|
+
- spec/spec_helper.rb
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: http://wiki.github.com/josephwilk/pairwise
|
68
|
+
licenses: []
|
69
|
+
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options:
|
72
|
+
- --charset=UTF-8
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
version:
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: "0"
|
86
|
+
version:
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.3.5
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Turn large test data combinations into smaller ones
|
94
|
+
test_files:
|
95
|
+
- spec/pairwise/builder_spec.rb
|
96
|
+
- spec/pairwise/cli_spec.rb
|
97
|
+
- spec/pairwise_spec.rb
|
98
|
+
- spec/spec_helper.rb
|