dtf 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
+