dtf 0.2.1

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.
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2011 Michal Papis
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,66 @@
1
+ # Deryls Testing Framework
2
+
3
+ DTF is a pluggable framework for testing shell scripts (at least now).
4
+ DTF also is an umbrella which incorporates (eventually) multiple gems, each of which provides additional functionality
5
+ to DTF. DTF is the skeleton upon which all other dtf-* gems build.
6
+
7
+
8
+ ## Usage
9
+
10
+ $ gem install dtf
11
+ $ dtf <path/to/file>_comment_test.sh
12
+
13
+ ## Comment tests
14
+
15
+ Filename has to end with _comment_test.sh
16
+
17
+ Example test file:
18
+
19
+ ## User comments start with double #
20
+ ## command can be writen in one line with multiple tests:
21
+ true # status=0; match=/^$/
22
+ ## or tests can be placed in following lines:
23
+ false
24
+ # status=1
25
+
26
+ ### Matchers
27
+
28
+ The test can be negated by replacing `=` with `!=`
29
+
30
+ - status=<number> - check if command returned given status (0 is success)
31
+ - match=/<regexp>/ - regexp match command output
32
+ - env[<var_name>]=/<regexp>/ - regexp match the given environment variable name
33
+
34
+ ## Example
35
+
36
+ $ bin/dtf example_tests/comment/*
37
+ F..
38
+ ##### Processed commands 2 of 2, success tests 2 of 3, failure tests 1 of 3.
39
+ $ false
40
+ # failed: status = 0 # was 1
41
+
42
+ $ bin/dtf example_tests/comment/* --text
43
+ ##### starting test failure.
44
+ $ false
45
+ # failed: status = 0 # was 1
46
+ ##### starting test success.
47
+ $ true
48
+ # passed: status = 0
49
+ # passed: status != 1
50
+ ##### Processed commands 2 of 2, success tests 2 of 3, failure tests 1 of 3.
51
+
52
+ ## Internal architecture
53
+
54
+ Framework will load plugins from any available gem and local `lib/` path, for example:
55
+
56
+ lib/plugins/dtf/text_output.rb
57
+ lib/plugins/dtf/status_test.rb
58
+ lib/plugins/dtf/comment_test_input.rb
59
+
60
+ The search pattern is:
61
+
62
+ lib/plugins/dtf/*.rb
63
+
64
+ And plugins are selected with:
65
+
66
+ lib/plugins/dtf/*_{input,test,output}.rb
data/bin/dtf ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ program_root = File.dirname( File.dirname( __FILE__ ) )
4
+
5
+ require "#{program_root}/lib/dtf"
6
+
7
+ exit DTF.new.run_tests ARGV
@@ -0,0 +1,94 @@
1
+ require 'rubygems'
2
+ require 'singleton'
3
+ require 'yaml'
4
+ require 'session'
5
+
6
+ lib_root = File.dirname( __FILE__ )
7
+
8
+ # include lib in path so plugins get found with Gem.find_files
9
+ $:.unshift "#{lib_root}"
10
+
11
+ class DTF; end
12
+ # load dtf/*.rb
13
+ Dir["#{lib_root}/dtf/*.rb"].each{|lib| require lib }
14
+
15
+ class DTF
16
+ def initialize
17
+ @ruby = File.join(RbConfig::CONFIG["bindir"],RbConfig::CONFIG["ruby_install_name"])
18
+ @plugins = DTF::Plugins.instance
19
+ @failures = 0
20
+ end
21
+
22
+ def run_tests args
23
+ @plugins.load(%w( all_test ))
24
+ input_files, not_processed = @plugins.parse_args(args)
25
+ if not_processed.size > 0
26
+ $stderr.puts "No plugin recognized this option '#{not_processed*" "}'."
27
+ exit 1
28
+ end
29
+ @plugins.load(%w( ErrorSummaryOutput )) if @plugins.output_plugins.empty?
30
+ process(input_files)
31
+ @failures == 0
32
+ end
33
+
34
+ def process input_files
35
+ @plugins.output_plugins(:start_processing)
36
+ input_files.each do |plugin,file|
37
+ process_test( plugin.load(file) )
38
+ end
39
+ @plugins.output_plugins(:end_processing)
40
+ end
41
+
42
+ def env shell
43
+ Hash[ shell.execute(
44
+ @ruby + ' -e \'ENV.each{|k,v| printf "#{k}=#{v}\0"}\''
45
+ )[0].split("\0").map{|var| var.split('=', 2) } ]
46
+ end
47
+
48
+ def process_test test
49
+ name, commands = test[:name], test[:commands]
50
+ shell = Session::Bash.new
51
+ _env = env(shell)
52
+ @plugins.output_plugins(:start_test, test, _env)
53
+ commands.each do |line|
54
+ command, tests = line[:cmd], line[:tests]
55
+ @plugins.output_plugins(:start_command, line)
56
+ _stdout = StringIO.new
57
+ _stderr = StringIO.new
58
+ _stdboth = StringIO.new
59
+ shell.execute "#{command}" do |out, err|
60
+ if out
61
+ @plugins.output_plugins(:command_out, out)
62
+ _stdout << out
63
+ _stdboth << out
64
+ end
65
+ if err
66
+ @plugins.output_plugins(:command_err, err)
67
+ _stderr << err
68
+ _stdboth << err
69
+ end
70
+ end
71
+ _status = shell.status
72
+ _env = env(shell)
73
+ @plugins.output_plugins(:end_command, line, _status, _env)
74
+ process_command_tests _stdout.string, _stderr.string, _stdboth.string, _status, _env, tests
75
+ end
76
+ @plugins.output_plugins(:end_test, test)
77
+ end
78
+
79
+ def process_command_tests _stdout, _stderr, _stdboth, _status, env, tests
80
+ tests.each do |test|
81
+ plugin = @plugins.test_plugins.find{|_plugin| _plugin.matches? test }
82
+ if plugin.nil?
83
+ status, msg = false, "Could not find plugin for test '#{test}'."
84
+ else
85
+ status, msg = plugin.execute(test, _stdout, _stderr, _stdboth, _status, env)
86
+ end
87
+ @failures+=1 unless status
88
+ @plugins.output_plugins(:test_processed, test, status, msg)
89
+ end
90
+ end
91
+
92
+ class << self
93
+ end
94
+ end
@@ -0,0 +1,21 @@
1
+ unless String.method_defined? :blank?
2
+ String.class_eval do
3
+ def blank?
4
+ self == ""
5
+ end
6
+ end
7
+ end
8
+ unless Array.method_defined? :blank?
9
+ Array.class_eval do
10
+ def blank?
11
+ size == 0
12
+ end
13
+ end
14
+ end
15
+ unless NilClass.method_defined? :blank?
16
+ NilClass.class_eval do
17
+ def blank?
18
+ true
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,102 @@
1
+ class DTF::Plugins
2
+ include Singleton
3
+
4
+ def initialize
5
+ detect
6
+ @additional_plugins = []
7
+ @input_plugins = []
8
+ @output_plugins = []
9
+ @test_plugins = []
10
+ end
11
+
12
+ def detect
13
+ @plugins = Gem.find_files('plugins/dtf/*.rb')
14
+ end
15
+
16
+ def add plugin
17
+ @additional_plugins << plugin
18
+ end
19
+
20
+ def delete plugin
21
+ @additional_plugins.delete plugin
22
+ end
23
+
24
+ def file_to_class item
25
+ File.basename(item,'.rb').capitalize.gsub(/_(.)/){ $1.upcase }
26
+ end
27
+
28
+ def list pattern=nil
29
+ # collect to lists
30
+ _list = @plugins + @additional_plugins
31
+ # filter by pattern if given
32
+ _list = _list.select{|item| item.match("_#{pattern}.rb$") } unless pattern.nil?
33
+ # get path and class name
34
+ _list.map!{|item| [ item, file_to_class(item), pattern ] }
35
+ # TODO: limit plugin versions (highest || use bundler)
36
+ _list.each{|item, klass, type| require item }
37
+ _list
38
+ end
39
+
40
+ def load wanted
41
+ [ :input, :test, :output ].each do |type|
42
+ _list = list(type)
43
+ if ! wanted.include?("all") && ! wanted.include?("all_#{type}")
44
+ _list = _list.select{|item, klass, _type| wanted.include?(klass) }
45
+ end
46
+ _list.each{|item, klass, _type|
47
+ klass = DTF.const_get(klass)
48
+ instance_variable_get("@#{type}_plugins".to_sym) << klass.new
49
+ }
50
+ end
51
+ end
52
+
53
+ def match_arg_klass arg, klass, type
54
+ klass = DTF.const_get(klass)
55
+ return nil unless klass.respond_to? :argument_matches?
56
+ matches = klass.argument_matches? arg
57
+ return nil if matches.nil?
58
+ matches.each do |match|
59
+ case match
60
+ when :load
61
+ instance_variable_get("@#{type}_plugins".to_sym) << klass.new
62
+ when :input
63
+ @input_files << [klass.new, arg]
64
+ else
65
+ return nil
66
+ end
67
+ end
68
+ return matches
69
+ rescue NameError
70
+ return nil
71
+ end
72
+
73
+ def parse_args args
74
+ @input_files, not_processed = [], []
75
+ available_plugins = [ :input, :test, :output ].map{ |type| list(type) }.flatten(1)
76
+ args.each do |arg|
77
+ matched = available_plugins.map do |item, klass, type|
78
+ match_arg_klass arg, klass, type
79
+ end.flatten.reject(&:nil?)
80
+ if matched.empty?
81
+ not_processed << arg
82
+ end
83
+ end
84
+ [ @input_files, not_processed ]
85
+ end
86
+
87
+ def input_plugins
88
+ @input_plugins
89
+ end
90
+
91
+ def output_plugins *args
92
+ if args.empty?
93
+ @output_plugins
94
+ else
95
+ @output_plugins.each{|plugin| plugin.send(*args) }
96
+ end
97
+ end
98
+
99
+ def test_plugins
100
+ @test_plugins
101
+ end
102
+ end
@@ -0,0 +1,40 @@
1
+ class DTF::CommentTestInput
2
+ def initialize
3
+ end
4
+
5
+ def self.argument_matches? argument
6
+ if argument =~ /_comment_test\.sh$/ && File.exist?(argument)
7
+ [:load, :input]
8
+ else
9
+ nil
10
+ end
11
+ end
12
+
13
+ def load file_name
14
+ lines = []
15
+ File.readlines(file_name).each{|line|
16
+ # Fix jruby-1.6.6-d19 bug with empty strings from files
17
+ line = "#{line}"
18
+ # remove human comments
19
+ line.sub!(/##.*$/,'')
20
+ # reject empty lines
21
+ line.strip!
22
+ next if line =~ /^$/
23
+ # extract command and tests
24
+ cmd, tests = line.split("#")
25
+ cmd.strip!
26
+ tests = if tests.blank?
27
+ []
28
+ else
29
+ tests.split(";").map(&:strip)
30
+ end
31
+ if cmd.blank?
32
+ lines.last[:tests] += tests unless lines.last.nil?
33
+ else
34
+ lines << { :cmd => cmd, :tests => tests }
35
+ end
36
+ }
37
+ name = file_name.gsub(/^.*\//,'').sub(/_comment_test\.sh$/,'')
38
+ { :name => name, :commands => lines }
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ class DTF::EnvMatchTest
2
+ MATCHER = /^env\[(.*)\]([!]?=)[~]?\/(.*)\//
3
+
4
+ def matches? test
5
+ test =~ DTF::EnvMatchTest::MATCHER
6
+ end
7
+
8
+ def execute test, _stdout, _stderr, _stdboth, _status, env
9
+ test =~ DTF::EnvMatchTest::MATCHER
10
+ variable, sign, value = $1.strip, $2, $3
11
+ var_val = env[ variable ]
12
+ if ( sign == "=" ) ^ ( Regexp.new(value) =~ "#{var_val}" )
13
+ [ false, "failed: env #{variable} #{sign} /#{value}/ # was '#{var_val}'" ]
14
+ else
15
+ [ true, "passed: env #{variable} #{sign} /#{value}/" ]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,93 @@
1
+ class DTF::ErrorSummaryOutput
2
+ RED = `tput setaf 1`
3
+ GREEN = `tput setaf 2`
4
+ YELLOW = `tput setaf 3`
5
+ BLUE = `tput setaf 4`
6
+ RESET = `tput setaf 9`
7
+
8
+ def self.argument_matches? argument
9
+ [:load] if argument == "--dotted"
10
+ end
11
+
12
+ def initialize output=nil
13
+ @counts={}
14
+ @counts[:commands] = 0
15
+ @counts[:tests] = 0
16
+ @counts[:commands_started] = 0
17
+ @counts[:commands_finished] = 0
18
+ @counts[:tests_success] = 0
19
+ @counts[:tests_failure] = 0
20
+ @counter_id = 0
21
+ @summary = {}
22
+ @output = output || $stdout
23
+ end
24
+
25
+ def start_processing
26
+ end
27
+
28
+ def status
29
+ text = "#{BLUE}##### Processed commands #{@counts[:commands_finished]} of #{@counts[:commands]}"
30
+ if @counts[:tests_success] > 0
31
+ text += ", #{GREEN}success tests #{@counts[:tests_success]} of #{@counts[:tests]}"
32
+ end
33
+ if @counts[:tests_failure] > 0
34
+ text += ", #{RED}failure tests #{@counts[:tests_failure]} of #{@counts[:tests]}"
35
+ end
36
+ skipped = @counts[:tests] - @counts[:tests_success] - @counts[:tests_failure]
37
+ if skipped > 0
38
+ text += ", #{YELLOW}skipped tests #{skipped} of #{@counts[:tests]}"
39
+ end
40
+ text += ".#{RESET}"
41
+ text
42
+ end
43
+
44
+ def summary
45
+ @summary.sort{|a,b| ak,_=a ; bk,_=b ; ak <=> bk }.each{|k,v|
46
+ @output.puts "#{YELLOW}$ #{v[:cmd]}#{RESET}"
47
+ v[:failed_tests].each{|t| puts "#{RED}# #{t}#{RESET}" }
48
+ }
49
+ text = ""
50
+ text
51
+ end
52
+
53
+ def end_processing
54
+ @output.printf "\n"
55
+ @output.puts status
56
+ @output.puts summary
57
+ end
58
+
59
+ def start_test test, env
60
+ @counts[:commands] += test[:commands].size
61
+ tests_counts = test[:commands].map{|line| line[:tests].nil? ? 0 : line[:tests].size }
62
+ @counts[:tests] += tests_counts.empty? ? 0 : tests_counts.inject(&:+)
63
+ end
64
+
65
+ def end_test test
66
+ end
67
+
68
+ def start_command line
69
+ @counts[:commands_started] += 1
70
+ @current_line = line.merge(:counter_id => @counts[:commands_started])
71
+ end
72
+
73
+ def end_command line, status, env
74
+ @counts[:commands_finished] += 1
75
+ end
76
+
77
+ def command_out out
78
+ end
79
+
80
+ def command_err err
81
+ end
82
+
83
+ def test_processed test, status, msg
84
+ @output.printf status ? "." : "F"
85
+ if status
86
+ @counts[:tests_success] += 1
87
+ else
88
+ @counts[:tests_failure] += 1
89
+ @summary[@current_line[:counter_id]] ||= @current_line.merge({:failed_tests=>[]})
90
+ @summary[@current_line[:counter_id]][:failed_tests] << msg
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,17 @@
1
+ class DTF::OutputMatchTest
2
+ MATCHER = /^match([!]?=)[~]?\/(.*)\//
3
+
4
+ def matches? test
5
+ test =~ DTF::OutputMatchTest::MATCHER
6
+ end
7
+
8
+ def execute test, _stdout, _stderr, _stdboth, _status, env
9
+ test =~ DTF::OutputMatchTest::MATCHER
10
+ sign, value = $1, $2
11
+ if ( sign == "=" ) ^ ( Regexp.new(value) =~ "#{_stdboth}" )
12
+ [ false, "failed: match #{sign} /#{value}/" ]
13
+ else
14
+ [ true, "passed: match #{sign} /#{value}/" ]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,73 @@
1
+ class DTF::StatsOutput
2
+ RED = `tput setaf 1`
3
+ GREEN = `tput setaf 2`
4
+ YELLOW = `tput setaf 3`
5
+ BLUE = `tput setaf 4`
6
+ RESET = `tput setaf 9`
7
+
8
+ def self.argument_matches? argument
9
+ [:load] if argument == "--text"
10
+ end
11
+
12
+ def initialize
13
+ @counts={}
14
+ @counts[:commands] = 0
15
+ @counts[:tests] = 0
16
+ @counts[:commands_finished] = 0
17
+ @counts[:tests_success] = 0
18
+ @counts[:tests_failure] = 0
19
+ end
20
+
21
+ def start_processing
22
+ end
23
+
24
+ def status
25
+ text = "#{BLUE}##### Processed commands #{@counts[:commands_finished]} of #{@counts[:commands]}"
26
+ if @counts[:tests_success] > 0
27
+ text += ", #{GREEN}success tests #{@counts[:tests_success]} of #{@counts[:tests]}"
28
+ end
29
+ if @counts[:tests_failure] > 0
30
+ text += ", #{RED}failure tests #{@counts[:tests_failure]} of #{@counts[:tests]}"
31
+ end
32
+ skipped = @counts[:tests] - @counts[:tests_success] - @counts[:tests_failure]
33
+ if skipped > 0
34
+ text += ", #{YELLOW}skipped tests #{skipped} of #{@counts[:tests]}"
35
+ end
36
+ text += ".#{RESET}"
37
+ text
38
+ end
39
+
40
+ def end_processing
41
+ puts status
42
+ end
43
+
44
+ def start_test test, env
45
+ @counts[:commands] += test[:commands].size
46
+ tests_counts = test[:commands].map{|line| line[:tests].nil? ? 0 : line[:tests].size }
47
+ @counts[:tests] += tests_counts.empty? ? 0 : tests_counts.inject(&:+)
48
+ end
49
+
50
+ def end_test test
51
+ end
52
+
53
+ def start_command line
54
+ end
55
+
56
+ def end_command line, status, env
57
+ @counts[:commands_finished] += 1
58
+ end
59
+
60
+ def command_out out
61
+ end
62
+
63
+ def command_err err
64
+ end
65
+
66
+ def test_processed test, status, msg
67
+ if status
68
+ @counts[:tests_success] += 1
69
+ else
70
+ @counts[:tests_failure] += 1
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ class DTF::StatusTest
2
+ MATCHER = /^status([!]?=)([[:digit:]]+)$/
3
+
4
+ def matches? test
5
+ test =~ DTF::StatusTest::MATCHER
6
+ end
7
+
8
+ def execute test, _stdout, _stderr, _stdboth, _status, env
9
+ test =~ DTF::StatusTest::MATCHER
10
+ sign, value = $1, $2.to_i
11
+ if ( sign == "=" ) ^ ( _status == value )
12
+ [ false, "failed: status #{sign} #{value} # was #{_status}" ]
13
+ else
14
+ [ true, "passed: status #{sign} #{value}" ]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ class DTF::TextOutput
2
+ RED = `tput setaf 1`
3
+ GREEN = `tput setaf 2`
4
+ YELLOW = `tput setaf 3`
5
+ BLUE = `tput setaf 4`
6
+ RESET = `tput setaf 9`
7
+
8
+ def self.argument_matches? argument
9
+ [:load] if argument == "--text"
10
+ end
11
+
12
+ def initialize
13
+ end
14
+
15
+ def start_processing
16
+ end
17
+
18
+ def end_processing
19
+ end
20
+
21
+ def start_test test, env
22
+ puts "#{BLUE}##### starting test #{test[:name]}.#{RESET}"
23
+ end
24
+
25
+ def end_test test
26
+ #puts "#{BLUE}##### finished test #{test[:name]}.#{RESET}"
27
+ end
28
+
29
+ def start_command line
30
+ puts "#{YELLOW}$ #{line[:cmd]}#{RESET}"
31
+ end
32
+
33
+ def end_command line, status, env
34
+ #puts ": $?=#{status}"
35
+ end
36
+
37
+ def command_out out
38
+ puts out
39
+ end
40
+
41
+ def command_err err
42
+ puts err
43
+ end
44
+
45
+ def test_processed test, status, msg
46
+ if status
47
+ puts "#{GREEN}# #{msg}#{RESET}"
48
+ else
49
+ puts "#{RED}# #{msg}#{RESET}"
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dtf
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Deryl R. Doucette
14
+ - Michal Papis
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2012-05-08 00:00:00 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: session
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ version: "3.0"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ description: Testing Framework solely based on plugins. For now only tests using Bash.
37
+ email: mpapis+dtf@gmail.com
38
+ executables:
39
+ - dtf
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - lib/dtf.rb
46
+ - lib/dtf/active_patches.rb
47
+ - lib/dtf/plugins.rb
48
+ - lib/plugins/dtf/output_match_test.rb
49
+ - lib/plugins/dtf/error_summary_output.rb
50
+ - lib/plugins/dtf/stats_output.rb
51
+ - lib/plugins/dtf/text_output.rb
52
+ - lib/plugins/dtf/status_test.rb
53
+ - lib/plugins/dtf/env_match_test.rb
54
+ - lib/plugins/dtf/comment_test_input.rb
55
+ - bin/dtf
56
+ - LICENSE
57
+ - README.md
58
+ homepage: http://github.com/dtf-gems/dtf
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options: []
63
+
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.23
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Deryl Testing Framework
91
+ test_files: []
92
+