├── .document ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.rdoc ├── Rakefile ├── VERSION ├── attest.gemspec ├── bin └── attest ├── doodle.txt ├── examples ├── basic_functionality_example.rb ├── mocha_example.rb ├── module_example.rb └── more │ ├── multiple_context_example.rb │ └── nesting │ └── expectations_as_tests_example.rb ├── lib ├── attest.rb ├── attest │ ├── config.rb │ ├── core_ext │ │ ├── kernel.rb │ │ ├── object.rb │ │ └── proc.rb │ ├── execution_context.rb │ ├── expectation_result.rb │ ├── interface │ │ ├── output_writer_configurator.rb │ │ ├── possible_tests_configurator.rb │ │ └── test_double_configurator.rb │ ├── output │ │ ├── basic_output_writer.rb │ │ ├── failures_only_output_writer.rb │ │ ├── output_writer.rb │ │ ├── output_writer_interface.rb │ │ └── test_unit_output_writer.rb │ ├── proc │ │ └── proc_source_reader.rb │ ├── rake │ │ └── attesttask.rb │ ├── test_container.rb │ ├── test_loader.rb │ ├── test_object.rb │ └── test_parser.rb └── trollop.rb └── spec ├── interface ├── output_writer_configurator_test.rb ├── possible_tests_configurator_test.rb └── test_double_configurator_test.rb ├── output └── output_writer_test.rb └── tmp └── new_require_test.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | vendor 23 | .bundle 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | #gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | attest (0.2.0) 5 | mocha 6 | trollop 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | mocha (0.9.9) 12 | rake 13 | rake (0.8.7) 14 | trollop (1.16.2) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | attest! 21 | mocha 22 | trollop 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Alan Skorkin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Attest 2 | 3 | attest (vb) - to affirm the correctness or truth of 4 | 5 | Attest allows you to define spec-like tests inline (within the same file as your actual code) which means almost non-existant overheads to putting some tests around your code. Of course that is just a feature that I wanted, you can just as easily put the tests into a separate file for a more traditional experience. Overall, attest tries to not be too prescriptive regarding the 'right' way to test. You want to test private methods - go ahead, access unexposed instance variables - no worries, pending and disabled tests are first class citizens. Don't like the output format, use a different one or write your own. You should be allowed to test your code the way you want to, not the way someone else says you have to! 6 | 7 | == A Quick Example 8 | 9 | Below is an example of how to use the basic functionality, everything is reasonably straight forward, there are more examples in the examples directory including this one: 10 | 11 | class ACalculator 12 | def remember_value(value) 13 | @value_in_memory = value 14 | end 15 | 16 | def increment(value) 17 | value + 1 18 | end 19 | 20 | def divide(numerator, denominator) 21 | numerator/denominator 22 | end 23 | 24 | def add(value1, value2) 25 | value1 + value2 26 | end 27 | 28 | private 29 | def multiply(x, y) 30 | x * y 31 | end 32 | end 33 | 34 | if ENV["attest"] 35 | this_tests ACalculator do 36 | before_each{@calculator = ACalculator.new} 37 | after_each{@calculator = nil} 38 | 39 | test("a pending test") 40 | test("deliberately fail the test"){should_fail} 41 | test("a successful empty test"){} 42 | test("should NOT raise an error") {should_not_raise{@calculator.increment 4}} 43 | test("it should raise an error, don't care what kind") {should_raise {@calculator.divide 5, 0}} 44 | test("it should raise a ZeroDivisionError error with a message"){should_raise(ZeroDivisionError, :with_message => /divided by.*/){@calculator.divide 5, 0}} 45 | test("adding 5 and 2 does not equal 8") { should_not_be_true(@calculator.add(5,2) == 8) } 46 | test("this test will be an error when non-existant method called") {should_be_true{ @calculator.non_existant }} 47 | test("should be able to call a private method like it was public"){should_be_true(@calculator.multiply(2,2) == 4)} 48 | 49 | test "access an instance variable without explicitly exposing it" do 50 | @calculator.remember_value(5) 51 | should_be_true {@calculator.value_in_memory == 5} 52 | end 53 | 54 | test("multiple expectations in one test") do 55 | should_not_raise{@calculator.increment 4} 56 | should_raise{ @calculator.non_existant } 57 | end 58 | 59 | nosetup 60 | test("should not have access to calculator instance since run without setup"){should_be_true(@calculator == nil)} 61 | 62 | test("should_equal expectations with and without block") do 63 | should_equal 5, 5 64 | should_equal(5){5} 65 | should_not_equal 7, 8 66 | should_not_be_equal(8){9} 67 | end 68 | 69 | test("should_be_same and should_not_be_same with and without block") do 70 | should_be_same 5, 5 71 | should_not_be_same(5){5.0} 72 | string = "a" 73 | should_be_same_as(string, string) 74 | should_not_be_same_as("a"){string} 75 | end 76 | 77 | test("should_be_true without block") {should_be_true 5 == 5.0} 78 | test("should_not_be_true without block") {should_be_false 5 == 6} 79 | 80 | test("should_succeed test") do 81 | should_succeed 82 | end 83 | 84 | test("should_be a lambda based matcher test") do 85 | great = lambda{"great"} 86 | should_be(great){"great"} 87 | should_not_be(great){"bad"} 88 | 89 | joke = lambda{"knock, knock"} 90 | should_be_a(joke){"knock, knock"} 91 | should_not_be_a(joke){"hello"} 92 | end 93 | end 94 | end 95 | 96 | == A Note About Inline Tests 97 | 98 | The original premise was to define the tests inline, in the same file as the code, to that end we wrap the test code in an if statement controlled by an env variable. However this is not ideal, it does work, but if you have complex code it can cause issues. The issues are due to the fact that in order to execute the tests defined inline we potentially have to load the actual code under test twice, so if you have code that can potentially break because of this it likely will. For example alias chains are likely to cause spectacular death, method missing interactions might do the same. The point is, inline tests are only for simple code at the moment. In the future I am looking at parsing the test code separately at which point these issues will disappear and you will be able to play with inline tests fully. In the meantime, inline tests for simple code, otherwise do the traditional thing and split the test code out into its own file. If you do split out into separate file, you won't need to wrap the if statement with the env variable around your test code so there is some advantage to doing that. For an example see some of the test under the spec/ directory in the code these use attest to test some of its own classes (i.e. eating own dogfood). 99 | 100 | 101 | == How Stuff Works 102 | 103 | As per usual, to get some help: 104 | 105 | attest --help 106 | 107 | This should tell you how to launch stuff, but the short story is this. You need to provide some files and/or directories you want to include and optionally some you want to exclude e.g.: 108 | 109 | attest -i file1.rb file2.rb dir_with_ruby_files/ -e dir_with_ruby_files/file3.rb 110 | 111 | All the ruby files you provide will be used as is, all the directories will be trawled to find ruby files, in the end the system ends up with a bunch of ruby files to include and a bunch to exclude. The excludes trump the includes so if you have the same file in the includes list and the excludes it will be excluded. After we intersect the includes and the excludes we end up with a final list of files to execute. These will be deemed the test files and will be executed as such. 112 | 113 | If your file needs other ruby files for it to function (as they often do :)), you will need to set up the requires yourself. So if your test code is in a separate file, you will need to require the file under test and whatever else it needs to work properly. Of course if your test is inline then this is hopefully not an issue. 114 | 115 | Currently the output produces when running will look something like this: 116 | 117 | >~/projects/attest$ attest -i examples/basic_functionality_example.rb 118 | /home/alan/projects/attest/examples/basic_functionality_example.rb: 119 | ACalculator 120 | - a pending test [PENDING] 121 | - deliberately fail the test [FAILURE] 122 | 123 | /home/alan/projects/attest/examples/basic_functionality_example.rb:30 124 | 125 | - a successful empty test 126 | - should NOT raise an error 127 | - it should raise an error, don't care what kind 128 | - it should raise a ZeroDivisionError error with a message 129 | - adding 5 and 2 does not equal 8 130 | - this test will be an error when non-existant method called [ERROR] 131 | 132 | NoMethodError: undefined method `non_existant' for # 133 | 134 | - should be able to call a private method like it was public 135 | - access an instance variable without explicitly exposing it 136 | - multiple expectations in one test 137 | - should not have access to calculator instance since run without setup 138 | - should_equal expectations with and without block 139 | - should_be_same and should_not_be_same with and without block 140 | - should_be_true without block 141 | - should_not_be_true without block 142 | - should_succeed test 143 | - should_be a lambda based matcher test 144 | 145 | 146 | 18 tests 28 expectations 25 success 1 failure 1 error 1 pending 0 disabled 147 | Finished in 19.01 milliseconds 148 | 149 | The above output is produced by the Basic output format. There are two other output formats, the TestUnit and the FailuresOnly. The Basic one is used by default, but you can specify the others like this: 150 | 151 | attest -i examples/basic_functionality_example.rb -o TestUnit 152 | 153 | The above will produce output similar to: 154 | 155 | PF.....E.......... 156 | 157 | 18 tests 28 expectations 25 success 1 failure 1 error 1 pending 0 disabled 158 | Finished in 20.05 milliseconds 159 | 160 | In the above output the dots mean success, the letters can be F, E, P, D which stand for failure, error, pending, disabled - pretty simple. 161 | 162 | The FailuresOnly output writer ignores everything except failure and error and outputs those. 163 | 164 | == Test Double Integration 165 | 166 | Currently if you want to mock and stub, the only thing that is built in is mocha integration. Mocha is also the default value for test double integration which means you need the mocha gem installed. If you don't want mocking and stubbing you can disable when you execute: 167 | 168 | attest -i file1.rb -t none 169 | 170 | If you want to be explicit regarding mocha you can do: 171 | 172 | attest -i file1.rb -t mocha 173 | 174 | There is an example of using mocha with attest in the examples directory, have a look. I do have plans to perhaps integrate other test double frameworks, and possibly to build a simple one to complement attest, but that's for the future. In the meantime hopefully mocha should fulfill most needs. 175 | 176 | 177 | == Current And Upcoming Features 178 | 179 | - define tests inline 180 | - call private methods as if they were public 181 | - access instance variables as if they were exposed 182 | - three types of output writers with the possibility to define your own 183 | - expectations such as should_fail, should_be_true, should_not_be_true, should_raise, should_not_raise, should_equal, should_be_same, should_match with aliases to other sensible names such as should_be_same_as etc. these work using both parameters or if you need to do fancy code, blocks 184 | - setup and teardown for all tests, but can configure a test so that no setup is run for it, also there is a before_all and after_all that gets executed once before/after all the tests in a file 185 | - automatically create a class that includes a module which methods you want to test (if you have a module called MyModule and you want to test its methods buy only after the module has been included in a class, a class called MyModuleClass will be automatically created and will include your module, an object of this new class will be instantiated for you to play with) e.g: 186 | 187 | before_all do 188 | @module_class = create_and_include(MyModule) 189 | end 190 | 191 | - tries not to pollute core objects too much 192 | - test can be pending if they have no body 193 | - tests can be marked as disabled 194 | - there is a rake task that is part of attest you can configure it in your project and execute your attest tests using rake instead of using the command line, the advantage is you configure once instead of every time you run, the disadvantage is less flexibility, i am looking to build this area out a bit more in future so you'll be able to do more with rake, here is an example of how to configure it: 195 | 196 | require 'attest/rake/attesttask' 197 | Rake::AttestTask.new do |attest| 198 | attest.include = ["spec/"] 199 | attest.exclude = ["spec/tmp"] 200 | attest.outputwriter = "Basic" 201 | attest.testdouble = "mocha" 202 | end 203 | 204 | - mocha integration to provide test double (mocking/stubbing) functionality, although I have a suspicion that it may not play nice with bundler the way it is now (need to confirm this), but if you have the gem installed normally everything should work fine, other test double frameworks are on the way 205 | 206 | I've got a few things I want to try out in the future, like the ability to define your own output writers, more smarts in various areas, state across test executions, ability to execute only specific tests e.g. slowest from previous run etc. More specifically here are some things that are high on my radar: 207 | 208 | - writing my own test double project in the same general style as attest and integrating with it as well to give another more seamless mock/stub option 209 | - allow for multiple setup and teardown blocks and ability to specify which tests they are relevant for 210 | - haven't yet decided if I nested contexts are a good idea, I find they tend to confuse things 211 | - maybe ability to do shared contexts, once again haven't decided if they are a good idea 212 | - more types of expectations for convenience such as predicate expectations and more smarts regarding difining your own matchers 213 | 214 | 215 | == More Examples 216 | 217 | Go to the examples directory in the code, it contains the above example as well as a couple of others, reasonably easy to understand. Also the spec directory contains some unit tests that utilise attest to test its own classes, this has examples of using mocha and fakefs as well as many of the features used in a real context. 218 | 219 | == If You Have Questions/Found A Bug/Want A Feature 220 | 221 | I am pretty open to ideas/conversation etc., just drop me a line (either through github or direct, just google skorks, you can't miss me :)) and we'll sort it out one way or another. 222 | 223 | == Note on Patches/Pull Requests 224 | 225 | * Fork the project. 226 | * Make your feature addition or bug fix. 227 | * Add tests for it. This is important so I don't break it in a future version unintentionally. If you can add some unit tests using attest itself then please do so, if it's too hard at least provide an example in the examples directory. 228 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 229 | * Send me a pull request. Bonus points for topic branches. 230 | 231 | == Copyright 232 | 233 | Copyright (c) 2010 Alan Skorkin. See LICENSE for details. 234 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "attest" 8 | gem.summary = %Q{An inline unit testing/spec framework that doesn't force you to follow arbitrary rules} 9 | gem.description = %Q{Attest allows you to define spec-like tests inline (within the same file as your actual code) which means almost non-existant overheads to putting some tests around your code. It also tries to not be too prescriptive regarding the 'right' way to test. You want to test private methods - go ahead, access unexposed instance variables - no worries, pending and disabled tests are first class citizens. Don't like the output format, use a different one or write your own. Infact you don't even have to define you tests inline if you prefer the 'traditional' way, separate directory and all. You should be allowed to test your code the way you want to, not the way someone else says you have to!} 10 | gem.email = "alan@skorks.com" 11 | gem.homepage = "http://github.com/skorks/attest" 12 | gem.authors = ["Alan Skorkin"] 13 | #gem.add_runtime_dependency "bundler" 14 | gem.add_development_dependency "mocha" 15 | gem.add_development_dependency "fakefs" 16 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 17 | end 18 | Jeweler::GemcutterTasks.new 19 | rescue LoadError 20 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 21 | end 22 | 23 | require 'rake/testtask' 24 | Rake::TestTask.new(:test) do |test| 25 | test.libs << 'lib' << 'test' 26 | test.pattern = 'test/**/test_*.rb' 27 | test.verbose = true 28 | end 29 | 30 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '/lib/'))) unless $:.include?(File.expand_path(File.join(File.dirname(__FILE__), '/lib'))) 31 | require 'attest/rake/attesttask' 32 | Rake::AttestTask.new do |attest| 33 | attest.include = ["spec/"] 34 | attest.exclude = ["spec/tmp"] 35 | attest.outputwriter = "Basic" 36 | attest.testdouble = "mocha" 37 | end 38 | 39 | begin 40 | require 'rcov/rcovtask' 41 | Rcov::RcovTask.new do |test| 42 | test.libs << 'test' 43 | test.pattern = 'test/**/test_*.rb' 44 | test.verbose = true 45 | end 46 | rescue LoadError 47 | task :rcov do 48 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 49 | end 50 | end 51 | 52 | task :test => :check_dependencies 53 | 54 | task :default => :test 55 | 56 | require 'rake/rdoctask' 57 | Rake::RDocTask.new do |rdoc| 58 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 59 | 60 | rdoc.rdoc_dir = 'rdoc' 61 | rdoc.title = "attest #{version}" 62 | rdoc.rdoc_files.include('README*') 63 | rdoc.rdoc_files.include('lib/**/*.rb') 64 | end 65 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /attest.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{attest} 8 | s.version = "0.2.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Alan Skorkin"] 12 | s.date = %q{2010-12-21} 13 | s.default_executable = %q{attest} 14 | s.description = %q{Attest allows you to define spec-like tests inline (within the same file as your actual code) which means almost non-existant overheads to putting some tests around your code. It also tries to not be too prescriptive regarding the 'right' way to test. You want to test private methods - go ahead, access unexposed instance variables - no worries, pending and disabled tests are first class citizens. Don't like the output format, use a different one or write your own. Infact you don't even have to define you tests inline if you prefer the 'traditional' way, separate directory and all. You should be allowed to test your code the way you want to, not the way someone else says you have to!} 15 | s.email = %q{alan@skorks.com} 16 | s.executables = ["attest"] 17 | s.extra_rdoc_files = [ 18 | "LICENSE", 19 | "README.rdoc" 20 | ] 21 | s.files = [ 22 | ".document", 23 | "Gemfile", 24 | "Gemfile.lock", 25 | "LICENSE", 26 | "README.rdoc", 27 | "Rakefile", 28 | "VERSION", 29 | "attest.gemspec", 30 | "bin/attest", 31 | "doodle.txt", 32 | "examples/basic_functionality_example.rb", 33 | "examples/mocha_example.rb", 34 | "examples/module_example.rb", 35 | "examples/more/multiple_context_example.rb", 36 | "examples/more/nesting/expectations_as_tests_example.rb", 37 | "lib/attest.rb", 38 | "lib/attest/config.rb", 39 | "lib/attest/core_ext/kernel.rb", 40 | "lib/attest/core_ext/object.rb", 41 | "lib/attest/core_ext/proc.rb", 42 | "lib/attest/execution_context.rb", 43 | "lib/attest/expectation_result.rb", 44 | "lib/attest/interface/output_writer_configurator.rb", 45 | "lib/attest/interface/possible_tests_configurator.rb", 46 | "lib/attest/interface/test_double_configurator.rb", 47 | "lib/attest/output/basic_output_writer.rb", 48 | "lib/attest/output/failures_only_output_writer.rb", 49 | "lib/attest/output/output_writer.rb", 50 | "lib/attest/output/output_writer_interface.rb", 51 | "lib/attest/output/test_unit_output_writer.rb", 52 | "lib/attest/proc/proc_source_reader.rb", 53 | "lib/attest/rake/attesttask.rb", 54 | "lib/attest/test_container.rb", 55 | "lib/attest/test_loader.rb", 56 | "lib/attest/test_object.rb", 57 | "lib/attest/test_parser.rb", 58 | "lib/trollop.rb", 59 | "spec/interface/output_writer_configurator_test.rb", 60 | "spec/interface/possible_tests_configurator_test.rb", 61 | "spec/interface/test_double_configurator_test.rb", 62 | "spec/output/output_writer_test.rb", 63 | "spec/tmp/new_require_test.rb" 64 | ] 65 | s.homepage = %q{http://github.com/skorks/attest} 66 | s.require_paths = ["lib"] 67 | s.rubygems_version = %q{1.3.7} 68 | s.summary = %q{An inline unit testing/spec framework that doesn't force you to follow arbitrary rules} 69 | s.test_files = [ 70 | "examples/basic_functionality_example.rb", 71 | "examples/mocha_example.rb", 72 | "examples/module_example.rb", 73 | "examples/more/multiple_context_example.rb", 74 | "examples/more/nesting/expectations_as_tests_example.rb", 75 | "spec/interface/output_writer_configurator_test.rb", 76 | "spec/interface/possible_tests_configurator_test.rb", 77 | "spec/interface/test_double_configurator_test.rb", 78 | "spec/output/output_writer_test.rb", 79 | "spec/tmp/new_require_test.rb" 80 | ] 81 | 82 | if s.respond_to? :specification_version then 83 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 84 | s.specification_version = 3 85 | 86 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 87 | s.add_development_dependency(%q, [">= 0"]) 88 | s.add_development_dependency(%q, [">= 0"]) 89 | else 90 | s.add_dependency(%q, [">= 0"]) 91 | s.add_dependency(%q, [">= 0"]) 92 | end 93 | else 94 | s.add_dependency(%q, [">= 0"]) 95 | s.add_dependency(%q, [">= 0"]) 96 | end 97 | end 98 | 99 | -------------------------------------------------------------------------------- /bin/attest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '../lib/'))) unless $:.include?(File.expand_path(File.join(File.dirname(__FILE__), '../lib'))) 4 | require 'trollop' 5 | 6 | opts = Trollop::options do 7 | banner <<-EOS 8 | Usage: 9 | attest [options] 10 | where [options] are: 11 | EOS 12 | opt :include, "Directories or files to search for tests, directories will be recursively expanded", :type => :strings, :required => true 13 | opt :exclude, "Directories or files to exclude when searching for tests, will override entries that have been included", :type => :strings 14 | opt :testdouble, "The identifier of the test double framework to use, can be 'none'", :type => :string, :default => "mocha" 15 | opt :outputwriter, "The output writer to use when running the tests", :type => :string, :default => "Basic" 16 | end 17 | 18 | require 'attest' 19 | require 'attest/interface/output_writer_configurator' 20 | require 'attest/interface/test_double_configurator' 21 | require 'attest/interface/possible_tests_configurator' 22 | 23 | Attest.configure do |config| 24 | config.output_writer = Attest::OutputWriterConfigurator.configure(opts[:outputwriter]) 25 | config.testdouble = Attest::TestDoubleConfigurator.configure(opts[:testdouble]) 26 | config.possible_tests = Attest::PossibleTestsConfigurator.configure(opts[:include], opts[:exclude]) 27 | end 28 | 29 | require 'attest/test_loader' 30 | 31 | Attest::TestLoader.execute(Attest.config.possible_tests, Attest.config.output_writer) 32 | -------------------------------------------------------------------------------- /doodle.txt: -------------------------------------------------------------------------------- 1 | rough features 2 | 3 | most basic 4 | - setup and teardown DONE 5 | - specify test for regular public method DONE 6 | - specify test for a private method DONE 7 | - ability to read instance variable of object under test without having to specify a reader DONE 8 | - the most basic result output DONE 9 | - able to detect when test expectation fails DONE 10 | - execute tests for single ruby file if any present DONE 11 | - able to detect when test errors out and gracefully say so in output DONE 12 | - all object to have a should_equal expectation method DONE 13 | - all objects should have a should_not_equal expectation method DONE 14 | 15 | - should be able to specify a test anonymously without providing description 16 | - ability to give a test an id as well as description 17 | - ability to specify that no setup or teardown should be run for the test 18 | - ability to specify multiple setups and teardowns and register specific setups and teardowns to run for specific tests 19 | - ability to magically include module in an object and test its methods while it is included in an object 20 | - should provide the error message if test errors out DONE 21 | - should provide the error trace if a test has errored DONE 22 | - all objects have access to a should_raise expectation DONE 23 | - should be able to produce a summary of execution at the end, with count of successes, failures and errors DONE 24 | - should be able to produce shorthand output while running with a summary at the end (test unit style shorthand), controlled by cli parameter 25 | - should have a should_fail expectation, possibly for tests that haven't been implemented aliased to not_implemented, not_implemented_failing, not_implemented_passing 26 | - should have a should_be_true expectation type for more flexibility DONE 27 | - test that are defined without a block should be tagged as not implemented in the output and in the summary 28 | - you can require the library without augmenting objects with extra methods, only when you call do objects get augmented and should test for the fact that a method we're trying to augment with already exists, alternatively, don't ever require this library directly then we don't have an issue DONE 29 | 30 | - the should methods should return an expectation object (itself) DONE 31 | - when deliberately failing should not actually print an error but should be a failure with a message instead DONE 32 | - the output writer should be a separate class to allow for different writers DONE 33 | - work out what methods need to be on the output writer class and hook them in where tests are run DONE 34 | - work out what other expectations need to be on the execution context intself (should_raise, should_be_true) DONE 35 | - work out how to easily implement negation of the positive expectations on the execution context 36 | - hook in configuration into the framework DONE 37 | - make it configurable via configuration and command line the name of the method that begins a test context (this_tests) 38 | - make it configurable via config and command line the name of the method that would produce an expectation object (itself or should), these are the only methods that extend core for now 39 | - try to execute existing method missing and if still an error then execute my method missing and fall back to existing method missing, so that don't accidentally kill method missing functionality that people wrote themselves 40 | - produce a short format output writer as well as a basic long format one to make sure that have ability to hook in different output writers 41 | - should be able to configure attest via file (attest.config in same directory, or give location of config as command line param, or via .attest file in home dir or via a configuration block in a file under test, each level can override the other) 42 | - what expectation methods are actaully needed on the objects in the test methods themselves (should_be_true, should_equal etc) work out negation for these ones as well 43 | - what methods should the expectation object have and how to allow to chain the methods and have everything work 44 | - all test context should have an id and if one is not provided it should be generated 45 | - all test methods should have an id and if one is not provided it should be generated 46 | - before and after blocks should be able to refer to tests they are targeting by the test id 47 | - test should be able to specify if they don't want the befores and afters to be run for them (this will take precedence over everything else) 48 | - need to have a should_fail execution context method, with alias of fail 49 | - should be able to call should_be empty on an object where empty is a method on the object, should_equal size 5 where size is a method on the object, i.e. boolean method on the objects gets used as a predicate 50 | - a expectation that this object as the same as another object, i.e. exactly the same object not an equal one 51 | - for exceptions, should_raise(XyzError) {x.some_call}.with_message(/some message regex/), the XyzError and the regex should be optional DONE 52 | - test methods in a module that is built for class inclusion 53 | - test free floating methods that don't belong to a class or a module 54 | - an anonymous test without description similat to the test pile below but still for only one thing 55 | - a test pile method where you want to test random methods or do lots of assertions at the same time without having to think of a name for it, should introspect somehow what is/will be run and work out descriptions from that 56 | - ability to define matchers as lambdas would be good i.e. obj.should_be happy where happy is a lambda that returns a boolean value 57 | 58 | 59 | 60 | - a rake task to run the tests instead of a separate executable, configuration should be done through the rake task 61 | - ability to mock stuff out using some of the popular mocking frameworks, ability to plug in different one by config, including my own by default which hasn't been written yet 62 | - potentially later on think about doing nested contexts as well as shared contexts 63 | - some way to potentially work out if files are ruby even if no .rb extension or alternatively pass a glob on the command line to match files 64 | - allow redefining of the name of the main context method through the command line(e.g. rather than this_tests allow person to name it whatever they want) 65 | 66 | 67 | - kill the This class and turn it back into a this_tests method on Kernel DONE 68 | - merge everything into Master DONE 69 | - branch master into a Hooks branch and leave to have a look at later DONE 70 | - remove the hooks stuff from master DONE 71 | - add the file in which the tests were found to the output of the tests DONE 72 | - add functionality to allow a directory with ruby files to be parsed for tests and all tests to be run if any are found, non-recursively DONE 73 | - add functionality to allow a directory to be parsed for tests recursively DONE 74 | - allow multiple context within the same file to work properly and execute properly DONE 75 | - make sure can test methods in a module not just in a class, magically included in a class DONE 76 | - make sure can test class methods DONE 77 | - make sure can test methods that don't belong to a module or a class DONE 78 | - when multiple contexts in the same file, only print the name of the file to the command line once DONE 79 | - don't print summaries after every context, only a totals summary at the end DONE 80 | - make it so that tests can take an options hash and one of the options should be to not run setup or teardown for the test, perhaps make it not part of the opts hash DONE 81 | - should be able to specify a test without a body for it to appear in the output as pending DONE 82 | - refactor all example code so it is a bit nicer and more representative DONE 83 | - once all the above are done, create some RDOC documentation to describe the basic features and usage of the framework DONE 84 | - also fix up the description and extended description, make sure everything looks proper as far as doco is concerned on github etc. DONE 85 | - if everything is ok at this point, do a first public release of the gem (give it an appropriately betaish version number) DONE 86 | - make sure the gem is available through rubyforge as well as wherever else is appropriate, e.g. gemcutter DONE 87 | 88 | 89 | next release 90 | 91 | $ test out with method missing defined on the class, does it still work ok, essentially call the original method missing first, catch appropriate exception out of there and then run my method missing code and if still no good then propagate the original exception DONE 92 | $ make sure if method missing is defined it is tried out first before the new_method_missing on kernel takes over (this is very similar to the one above), need a test here 93 | $ integrate mocha so that can mock using mocha, this should be relatively straight forward DONE 94 | $ mocha should be set up as a configurable parameter even though it will be the only test double capability available DONE 95 | $ try out using mocha within the framework on another project totally separate, to nut out how it should be not made a runtime dependency but can be used when pulled in to the other project as a development dependency or a default dependency, also if not using bundler at all and it is not installed, what happens DONE 96 | $ refactor how mocha gets included into the project DONE 97 | $ make sure the binary is only used to read the command line and calls down to other methods to actually load the test files and begin execution of all the tests including outputting the summary at the end DONE 98 | $ output calculations of expectations not just tests, relatively straight forward and is a good idea DONE 99 | $ allow expectations to be specified directly as tests (perhaps failures should contain the block that has failed), the test passes and fails and the floating expectation passes and fails will probably need to be reported separately DONE 100 | $ if we get a test failure, perhaps more of a message regarding where/why the failure occured i.e. which expectation actually failed DONE 101 | $ create a failures only output writer DONE 102 | $ create a test/unit style output writer the one with the dots DONE 103 | $ allow the different types of output writer to be configured as the one to use via command line parameter DONE 104 | $ should be able to disable a test so that it is not executed, but is listed as disabled in the output DONE 105 | $ refactor the command line handling to split out actual cli stuff from the attest code that acts on the cli params DONE 106 | $ should be able to configure attest to be used without any test double framework at all DONE 107 | $ create a better examples directory more consistent test naming etc DONE 108 | $ combine assertion reporting and test reporting, we only really care about assertion reporting, no need to print out separate line for both DONE 109 | $ ability to provide multiple directories, files from which to pull the possible tests DONE 110 | $ ability to provide multiple directories, file which should be excluded from possible tests DONE 111 | $ need to refactor my execution context for less duplication and more consistency DONE 112 | $ rename before_all to be before each DONE 113 | $ need a before_each and a before_all which gets executed before all the test in the group, currently have a before_all which really acts like a before_each not good DONE 114 | $ possible bug or unexpected feature, if test contains no assertions in the output then it is treated by the output writer as having an implicit assertion/expectation (a success expectation) this is ok I think DONE 115 | $ currently unable to specify expectations in before_all and after_all and have them be picked up, this should work, expectations should belong to the test container rather than to any specific test object, on second thoughts maybe shouldn't have expectations on before and after all DONE 116 | $ should be able to specify expectations on the before_each and after_each DONE 117 | $ more different types of expectations, should_equal(5){5}, should_not_equal, should_be_same(obj), should_be_same_as, should_not_be_same, should_not_be_same_as, should_not_fail, should_pass, should_be_around(6){5}.plus_minus(1), should_be_a/an(some_lambda){"hello"}, should_match/should_match_a/should_match_an, more thought regarding how to specify a custom matcher, something for arrays and hashes, should_include(some_array), perhaps ability to do blocks and do without blocks i.e. should_include(5, some_array) DONE 118 | $ refactor expectations so that they can be used without a block but simply with parameters DONE 119 | $ need to refactor my output writers to extract all common functionality so that there isn't so much code duplication (perhaps using modules or something) DONE 120 | $ create a rake task that can be easily configured into a rake file with all the relevant configuration values so that attest can be run through rake instead of just the command line DONE 121 | $ timing for how long it took to complete the whole test suite DONE 122 | $ better failure reporting DONE 123 | $ modify the readme, mention the problems wil inline test definition, mention the new expectations, mentiont about the new output writers, mention how output reporting works, disabled tests, test double framework configuration, inclusion and exclusion, before_all and before_each, mention the rake task DONE 124 | 125 | 126 | $ BUG: I think I am now counting expectations passes rather than test passes, since i am getting ran 6 test 7 successful for mocka tests, need to look into this further DONE 127 | 128 | $ POC perhaps hold off until next release, ability to actually convert a block to a string to be able to provide more information regarding what actually failed, essentially will need proc.to_s code DONE 129 | $ POC remove trollop as a dependency to minimize dependecies, either that, or pull the trollop code in directly to minimize on dependecies, see if it's possible to easily acomplish this with git, if not then just manual DONE 130 | - POC once test double functionality is in, see what we can do about writing some tests for this framework using itself, even a minimal test suite will be better than nothing 131 | $ POC rake task that can easily be configured in a project to configure all the moving parts and run attest via rake (e.g. rake attest) DONE 132 | - POC expectations on objects themselves, see if this can be done in such a way as to make it possible to include that bit separately via a separate require or command line paramter or rake config or all of the above 133 | - POC hook in autotest somehow 134 | - POC write some smarts about requiring things maybe even do this as a separate gem similar to require_all but with even more smarts and potential configurations call it require_bot 135 | - POC own test double framework first only for ability to simply provide stubs, also ability to do mocks that verify expected behaviour, possibly two modes, one that complains when unexpected methods are called within test methods, the other mode is the opposite, is fine with unexpected methods, but complains when expected methods are not called, mock and stub class and instance methods, mock on just one object or all objects of the type, probably should be its own gem as well 136 | $ POC support to allow more traditional i.e. non-inline creating of test in which case perhaps the if env block should not be necessary, tests will live in an attest directory, whatever once again should work via executable as well as rake task, all the smarts that we need to get this to happen DONE 137 | 138 | next release 2 139 | 140 | - figure out if mocha works properly when using bundler, my guess it will not 141 | - if mocha not working properly get it to work with bundler correctly, maybe need to add a parameter to say which bundler group moacha is in 142 | - need to be able to define helper methods within tests somehow that can be called from within tests and setups etc. 143 | - timing for each test as well as each container should be recorded even if not outputted 144 | - refactor the duplication between the attest executable in bin and the attest rake task 145 | - figure out a better way to read/execute tests if they are defined inline, perhaps the file needs to be parsed lexically and the inline test bit pulled out and run as its own separate file, maybe through creating a temp file or even just able eval the block somehow without need for temp file, but will need to keep track of line numbers from original file somehow and will need to make sure that proc to string still works correctly in this case 146 | - as part of the spec test suite create a set of blackbox tests that replicate the functionality in the examples directory to test that actual functionality is working correctly, perhaps create some sort of testing output writer and use that for asserting stuff possibly, these will esssentially be acceptance tests 147 | - create a better set of unit tests, essentially need more unit tests, try to test what the methods do as opposed to simply testing that methods get called 148 | - still more matchers, should_be_around(6, :plus_minus => 1){5} that should work for floats also, maybe give ability to do predicate matchers, i.e if objects responds to a question mark method like include? should be able to do should_include 5, some_array, also think about a good way to create more flexible matchers, more flexible than lambda based ones so that can supply parameters to matchers 149 | - see if possible to integrate another test double framework besides mocha, like rr or shoulda etc. 150 | - allow for multiple before and after blocks should be able to essentially have specific before and after blocks run for specific tests, perhaps using some kind of id marker for tests 151 | - some way to share the expectations that you have created yourself amongs all the different tests in different files, as long as matchers can be specified in a separate file they can be shared using requires, perhaps matchers should be pre-parsed out of all the test files beforehand, which means we perhaps need to pre-parse all the tests before executing them 152 | - ability to run tests in parallel in multiple different ruby processes (depending on how many processors you have, configurable) then aggregate all the results, unit test parallelisation, ability to declare some test classes as non-parallelisable in which case they execute in a traditional fashion after all the parallelised tests have been run 153 | - ability to only run unsuccessful tests from a previous run, only the fails and the errors, the slowest test from previous run, this means that need to keep state of previous run perhaps save it in a file in the current project, location should be specifiable i.e. .attest.state 154 | - ability to configure the attest parameters via configuration file in the top level directory of the project i.e. .attest.config 155 | - build out the rake fucntionality with ability to run a single container/file, or even a single test from that file, perhaps by line number, all the other crazines should also be possible such as all failing tests from previous run, all error tests from previous run, slowest x tests from previous run 156 | - ability to run only the tests that were run on the previous run 157 | - hook in autotest to be able to have tests executing in a automatic fashion 158 | - sandboxify the execution context class that I am using a little bit more, by remove irrelevant methods etc. 159 | - update the readme with all the relevant new details 160 | - don't forget to bump the version before releasing the gem 161 | 162 | next release 3 163 | - pull in tests from other files into current file easily, all tests from another specification block should be able to be pulled into the current block, this may be of limited usefullness 164 | - allow expectations to be made on objects themselves, this needs further thought and perhaps can be controlled by parameter 165 | - write and integrate own test double framework, simple at first with basic stubbing and mocking functionality (look at rr for syntax suggestions) 166 | - come up with some more useful expectations, both on object and as standalone methods of test 167 | - look into rails stuff regarding expectations and idioms etc 168 | - should somehow be able to require all the necessary ruby files that are needed when a test is run for the test not to fail, e.g. because ruby files depend on other ruby files 169 | - look into doing helpful things for testing web stuffs 170 | - look into doing stuff to make railsy testing easier 171 | - build out the unit test suite fully 172 | - figure out a way to do acceptance level tests, i.e. black box functionality tests pehaps something that will build the attest gem, then install it the take a project with sample tests in it and copy it somewhere and then use attest to run the tests in that sample project the results should be written somewhere and not just outputted to the cli, but do need some way to set expectations of failure and success so that when new tests are added to the suite they get picked up automagically, perhaps some sort of debug/testing mode, but need a way for people to not be able to easily enable it, maybe this can be done through the description of the tests which can be parsed for what the expected result is 173 | - ability for people to define their own output writer and hook it in for attest to use somehow 174 | - output writer functionality should be separated more from the actual outputting of stuff so that if people define their own output writer they can simply reuse all the calculations without having to do it themselves 175 | - see how easy it would be to provide nested contexts what that would entail and how it would effect everything, should be reasonably straight forward with recursiveness etc, but will need to think about a more scalable way of nesting in the output writers etc., need to study how these work in more detail 176 | - see how easy it would be to provide shared contexts, need to study how these work in more detail 177 | - look at what unit testing frameworks provide as far as rails support is concerned and how easy it would be to provide similar support, i.e. maybe through railties or whatever, only for rails 3 etc 178 | 179 | bug 180 | - when requiring ruby files in another ruby file that one seems to inherit the tests from the other one DONE 181 | - have a look at doing a similar thing for load method, i.e. myself call new load with an introduced new paramter, and only in that case should the env var for attest be set otherwise even regular load should ignore the if attest block, only if it look slike being an issue 182 | -------------------------------------------------------------------------------- /examples/basic_functionality_example.rb: -------------------------------------------------------------------------------- 1 | class ACalculator 2 | def remember_value(value) 3 | @value_in_memory = value 4 | end 5 | 6 | def increment(value) 7 | value + 1 8 | end 9 | 10 | def divide(numerator, denominator) 11 | numerator/denominator 12 | end 13 | 14 | def add(value1, value2) 15 | value1 + value2 16 | end 17 | 18 | private 19 | def multiply(x, y) 20 | x * y 21 | end 22 | end 23 | 24 | if ENV["attest"] 25 | this_tests ACalculator do 26 | before_each{@calculator = ACalculator.new} 27 | after_each{@calculator = nil} 28 | 29 | test("a pending test") 30 | test("deliberately fail the test"){should_fail} 31 | test("a successful empty test"){} 32 | test("should NOT raise an error") {should_not_raise{@calculator.increment 4}} 33 | test("it should raise an error, don't care what kind") {should_raise {@calculator.divide 5, 0}} 34 | test("it should raise a ZeroDivisionError error with a message"){should_raise(ZeroDivisionError, :with_message => /divided by.*/){@calculator.divide 5, 0}} 35 | test("adding 5 and 2 does not equal 8") { should_not_be_true(@calculator.add(5,2) == 8) } 36 | test("this test will be an error when non-existant method called") {should_be_true{ @calculator.non_existant }} 37 | test("should be able to call a private method like it was public"){should_be_true(@calculator.multiply(2,2) == 4)} 38 | 39 | test "access an instance variable without explicitly exposing it" do 40 | @calculator.remember_value(5) 41 | should_be_true {@calculator.value_in_memory == 5} 42 | end 43 | 44 | test("multiple expectations in one test") do 45 | should_not_raise{@calculator.increment 4} 46 | should_raise{ @calculator.non_existant } 47 | end 48 | 49 | nosetup 50 | test("should not have access to calculator instance since run without setup"){should_be_true(@calculator == nil)} 51 | 52 | test("should_equal expectations with and without block") do 53 | should_equal 5, 5 54 | should_equal(5){5} 55 | should_not_equal 7, 8 56 | should_not_be_equal(8){9} 57 | end 58 | 59 | test("should_be_same and should_not_be_same with and without block") do 60 | should_be_same 5, 5 61 | should_not_be_same(5){5.0} 62 | string = "a" 63 | should_be_same_as(string, string) 64 | should_not_be_same_as("a"){string} 65 | end 66 | 67 | test("should_be_true without block") {should_be_true 5 == 5.0} 68 | test("should_not_be_true without block") {should_be_false 5 == 6} 69 | 70 | test("should_succeed test") do 71 | should_succeed 72 | end 73 | 74 | test("should_be a lambda based matcher test") do 75 | great = lambda{"great"} 76 | should_be(great){"great"} 77 | should_not_be(great){"bad"} 78 | 79 | joke = lambda{"knock, knock"} 80 | should_be_a(joke){"knock, knock"} 81 | should_not_be_a(joke){"hello"} 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /examples/mocha_example.rb: -------------------------------------------------------------------------------- 1 | class Product 2 | end 3 | 4 | if ENV["attest"] 5 | this_tests "testing of basic mocha integration" do 6 | test("mocking a class method") do 7 | product = Product.new 8 | Product.expects(:find).with(1).returns(product) 9 | should_be_true {product == Product.find(1)} 10 | end 11 | 12 | test("mocking an instance method") do 13 | product = Product.new 14 | product.expects(:save).returns(true) 15 | should_be_true{product.save} 16 | end 17 | 18 | test("stubbing an instance method") do 19 | prices = [stub(:pence => 1000), stub(:pence => 2000)] 20 | product = Product.new 21 | product.stubs(:prices).returns(prices) 22 | should_be_true{ [1000, 2000] == product.prices.collect {|p| p.pence}} 23 | end 24 | 25 | test("stubbing an all instances") do 26 | Product.any_instance.stubs(:name).returns('stubbed_name') 27 | product = Product.new 28 | should_be_true{ 'stubbed_name' == product.name } 29 | end 30 | 31 | test("traditional mocking") do 32 | object = mock() 33 | object.expects(:expected_method).with(:p1, :p2).returns(:result) 34 | should_be_true { :result == object.expected_method(:p1, :p2) } 35 | end 36 | 37 | test("shortcuts") do 38 | object = stub(:method1 => :result1, :method2 => :result2) 39 | should_be_true{ :result1 == object.method1 } 40 | should_be_true{ :result2 == object.method2 } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /examples/module_example.rb: -------------------------------------------------------------------------------- 1 | class StandardCalculator 2 | def self.plus(x, y) 3 | x + y 4 | end 5 | 6 | def self.minus(x, y) 7 | x - y 8 | end 9 | end 10 | 11 | module CalcModule 12 | def double(x) 13 | 2 * x 14 | end 15 | end 16 | 17 | if ENV["attest"] 18 | this_tests "another class with calculations" do 19 | test("adding two numbers") {should_be_true{StandardCalculator.plus(5,11) == 16}} 20 | test("subtracting two numbers"){should_not_be_true{StandardCalculator.minus(10,5) == 4}} 21 | end 22 | 23 | this_tests CalcModule do 24 | before_all do 25 | @hello = [1,2,3] 26 | end 27 | 28 | before_each { @module_class = create_and_include(CalcModule) } 29 | 30 | test("magically instance of a class that will include the module"){should_be_true{@module_class.double(5)==10}} 31 | 32 | nosetup 33 | test("another test without setup"){should_be_true{true}} 34 | 35 | disabled 36 | test("a disabled test"){should_be_true{true}} 37 | 38 | test("accessing object which was created in a before_all block") do 39 | should_be_true{@hello.size == 3} 40 | @hello << 5 41 | should_be_true{@hello.size == 4} 42 | end 43 | 44 | test("accessing object from before_all block again to make sure it is the same object") do 45 | should_be_true{@hello.last == 5} 46 | @hello << 10 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /examples/more/multiple_context_example.rb: -------------------------------------------------------------------------------- 1 | class Placeholder1 2 | def divide(x,y) 3 | x/y 4 | end 5 | end 6 | 7 | class Placeholder2 8 | def multiply(x,y) 9 | x*y 10 | end 11 | end 12 | 13 | def random_method(x) 14 | x+x 15 | end 16 | 17 | if ENV["attest"] 18 | this_tests Placeholder1 do 19 | test("divide successful"){should_be_true{Placeholder1.new.divide(16, 4) == 4}} 20 | test("divide by zero"){should_raise{Placeholder1.new.divide(5,0)}} 21 | end 22 | this_tests Placeholder2 do 23 | test("multiply") {should_be_true{Placeholder2.new.multiply(2,3)==6}} 24 | end 25 | this_tests "random methods" do 26 | test("random method"){should_be_true{random_method("abc") == "abcabc"}} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /examples/more/nesting/expectations_as_tests_example.rb: -------------------------------------------------------------------------------- 1 | class AssertionsAsTests 2 | class << self 3 | def five 4 | 5 5 | end 6 | 7 | def error 8 | raise 9 | end 10 | end 11 | end 12 | 13 | if ENV["attest"] 14 | this_tests "the fact that assertions can be specified as tests" do 15 | should_be_true{5 == AssertionsAsTests.five} 16 | should_raise do 17 | AssertionsAsTests.error 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/attest.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path(File.dirname(__FILE__))) unless $:.include?(File.expand_path(File.dirname(__FILE__))) 2 | 3 | require 'attest/config' 4 | 5 | module Attest 6 | class << self 7 | def configure 8 | config = Attest::Config.instance 9 | block_given? ? yield(config) : config 10 | end 11 | 12 | alias :config :configure 13 | 14 | Attest::Config.public_instance_methods(false).each do |name| 15 | self.class_eval <<-EOT 16 | def #{name}(*args) 17 | configure.send("#{name}", *args) 18 | end 19 | EOT 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/attest/config.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Attest 4 | class Config 5 | include Singleton 6 | 7 | attr_accessor :output_writer, :current_file, :testdouble, :possible_tests 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/attest/core_ext/kernel.rb: -------------------------------------------------------------------------------- 1 | require 'attest/test_parser' 2 | 3 | module Kernel 4 | def new_method_missing(name, *args, &block) 5 | original_error = nil 6 | begin 7 | return old_method_missing(name, *args, &block) 8 | rescue NoMethodError => e 9 | original_error = e 10 | end 11 | private_method = false 12 | instance_variable = false 13 | private_methods.each do |meth| 14 | private_method = true if meth == name 15 | end 16 | instance_variable = true if instance_variable_defined?("@#{name}".to_sym) 17 | if private_method 18 | send(name, *args, &block) 19 | elsif instance_variable 20 | self.class.class_eval do 21 | attr_reader name.to_sym 22 | end 23 | send(name, *args, &block) 24 | else 25 | raise NoMethodError, original_error.to_s, original_error.backtrace 26 | end 27 | end 28 | alias_method :old_method_missing, :method_missing 29 | alias_method :method_missing, :new_method_missing 30 | 31 | def new_require(filename) 32 | current_attest_value = ENV["attest"] 33 | ENV["attest"] = nil 34 | old_require(filename) 35 | ENV["attest"] = current_attest_value 36 | end 37 | alias_method :old_require, :require 38 | alias_method :require, :new_require 39 | 40 | private 41 | def this_tests(description="anonymous", &block) 42 | container = Attest::TestParser.new(description, block).parse 43 | container.execute_all 44 | end 45 | def current_method 46 | caller[0][/`([^']*)'/, 1] 47 | end 48 | def calling_method 49 | caller[1][/`([^']*)'/, 1] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/attest/core_ext/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | def hello 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/attest/core_ext/proc.rb: -------------------------------------------------------------------------------- 1 | current_dir = File.expand_path(File.dirname(__FILE__)) 2 | require "#{current_dir}/../proc/proc_source_reader" 3 | 4 | class Proc 5 | def to_string 6 | @source ||= Attest::ProcSourceReader.find(*source_descriptor) 7 | end 8 | 9 | private 10 | def source_descriptor 11 | unless @file && @line 12 | if md = /^#$/.match(inspect) 13 | @file, @line = md.captures 14 | end 15 | end 16 | [@file, @line.to_i] 17 | end 18 | end 19 | 20 | if __FILE__ == $0 21 | simple_proc = Proc.new() { |a| 22 | puts "Hello Rudy2" 23 | } 24 | puts simple_proc.to_string 25 | 26 | another_simple_proc = Proc.new() do |x| 27 | #printing 28 | puts "Printing #{x}" 29 | puts "printing more stuff" 30 | end 31 | puts another_simple_proc.to_string 32 | 33 | third_proc = eval "Proc.new do puts 'blah'; end" 34 | puts third_proc.to_string 35 | end 36 | -------------------------------------------------------------------------------- /lib/attest/execution_context.rb: -------------------------------------------------------------------------------- 1 | require 'attest/expectation_result' 2 | 3 | module Attest 4 | class ExecutionContext 5 | attr_reader :results 6 | 7 | class << self 8 | def assertions 9 | self.instance_methods(false).select{|method_name| method_name =~ /^should.*/ }.inspect 10 | end 11 | end 12 | 13 | def initialize(context=nil) 14 | @results = [] 15 | @subject = self 16 | @persistent_context = context 17 | own_instance_variables = self.instance_variables 18 | context.instance_variables.each do |instance_variable| 19 | unless own_instance_variables.include? instance_variable 20 | self.instance_variable_set(instance_variable.to_s, context.instance_variable_get(instance_variable)) 21 | end 22 | end 23 | end 24 | 25 | def should_be(expected_lambda, &block) 26 | with_new_result do |result| 27 | expected_lambda.call == yield ? result.success : result.failure 28 | end 29 | end 30 | alias :should_be_a :should_be 31 | alias :should_be_an :should_be 32 | alias :should_match :should_be 33 | alias :should_match_a :should_be 34 | alias :should_match_an :should_be 35 | 36 | def should_not_be(expected_lambda, &block) 37 | with_new_result do |result| 38 | expected_lambda.call != yield ? result.success : result.failure 39 | end 40 | end 41 | alias :should_not_be_a :should_not_be 42 | alias :should_not_be_an :should_not_be 43 | alias :should_not_match :should_not_be 44 | alias :should_not_match_a :should_not_be 45 | alias :should_not_match_an :should_not_be 46 | 47 | def should_be_same(expected_value, actual_value=nil, &block) 48 | derive_result_status_from_method(expected_value, actual_value, :"equal?", &block) 49 | end 50 | alias :should_be_same_as :should_be_same 51 | 52 | def should_not_be_same(expected_value, actual_value=nil, &block) 53 | derive_result_status_from_method_negated(expected_value, actual_value, :"equal?", &block) 54 | end 55 | alias :should_not_be_same_as :should_not_be_same 56 | 57 | def should_equal(expected_value, actual_value=nil, &block) 58 | derive_result_status_from_method(expected_value, actual_value, :"==", &block) 59 | end 60 | 61 | def should_not_equal(expected_value, actual_value=nil, &block) 62 | derive_result_status_from_method_negated(expected_value, actual_value, :"==", &block) 63 | end 64 | alias :should_not_be_equal :should_not_equal 65 | 66 | def should_be_true(actual_value=nil, &block) 67 | derive_result_status_from_method(true, actual_value, :"==", &block) 68 | end 69 | 70 | def should_not_be_true(actual_value = nil, &block) 71 | derive_result_status_from_method(false, actual_value, :"==", &block) 72 | end 73 | alias :should_be_false :should_not_be_true 74 | 75 | def should_fail 76 | with_new_result do |result| 77 | result.failure 78 | end 79 | end 80 | alias :should_not_succeed :should_fail 81 | 82 | def should_succeed 83 | with_new_result do |result| 84 | result.success 85 | end 86 | end 87 | alias :should_not_fail :should_succeed 88 | alias :should_pass :should_succeed 89 | 90 | def should_not_raise(&block) 91 | should_raise(&block) 92 | with_last_result do |result| 93 | result.success? ? result.failure : result.success 94 | end 95 | end 96 | 97 | #the only opt so far is :with_message which takes a regex 98 | def should_raise(type=nil, opts={}, &block) 99 | with_new_result do |result| 100 | begin 101 | if block_given? 102 | yield 103 | end 104 | rescue => e 105 | result.update(:expected_error => e) 106 | if expected_error?(type, opts[:with_message], e) 107 | result.success 108 | else 109 | result.failure 110 | end 111 | end 112 | unless result.success? 113 | result.failure 114 | end 115 | end 116 | end 117 | 118 | #worker methods 119 | def create_and_include(module_class) 120 | class_name = "#{module_class}Class" 121 | klass = nil 122 | begin 123 | klass = Object.const_get(class_name) 124 | rescue NameError => e 125 | class_instance = Class.new 126 | Object.const_set class_name, class_instance 127 | Object.const_get(class_name).include(Object.const_get("#{module_class}")) 128 | klass = Object.const_get(class_name) 129 | end 130 | klass.new 131 | end 132 | 133 | private 134 | def derive_result_status_from_method(expected_value, actual_value, method, negated=false, &block) 135 | with_new_result do |result| 136 | if block_given? 137 | method_return = expected_value.send(method, yield) 138 | method_return = !method_return if negated 139 | method_return ? result.success : result.failure 140 | else 141 | method_return = expected_value.send(method, actual_value) 142 | method_return = !method_return if negated 143 | method_return ? result.success : result.failure 144 | end 145 | end 146 | end 147 | 148 | def derive_result_status_from_method_negated(expected_value, actual_value, method, &block) 149 | derive_result_status_from_method(expected_value, actual_value, method, true, &block) 150 | end 151 | 152 | def source_location 153 | caller.each_with_index do |stack_line| 154 | if stack_line[Attest.config.current_file] 155 | return stack_line[/(.*:\d+):.*/, 1] 156 | end 157 | end 158 | "" 159 | end 160 | 161 | def with_new_result 162 | result = Attest::ExpectationResult.new 163 | yield result 164 | result.source_location = source_location 165 | @results << result 166 | end 167 | 168 | def with_last_result 169 | result = @results.last 170 | yield result 171 | result.source_location = source_location 172 | end 173 | 174 | def expected_error?(expected_type, expected_message_regex, actual_error) 175 | if expected_type.nil? && expected_message_regex.nil? || expected_error_type?(expected_type, actual_error.class) && expected_message_regex.nil? || expected_error_type?(expected_type, actual_error.class) && error_message_matches?(expected_message_regex, actual_error.message) 176 | return true 177 | else 178 | return false 179 | end 180 | end 181 | 182 | def expected_error_type?(expected_type, actual_type) 183 | expected_type == actual_type 184 | end 185 | 186 | def error_message_matches?(expected_message_regex, actual_error_message) 187 | actual_error_message =~ expected_message_regex 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /lib/attest/expectation_result.rb: -------------------------------------------------------------------------------- 1 | module Attest 2 | class ExpectationResult 3 | include Comparable 4 | class << self 5 | def status_types 6 | status_weights.keys 7 | end 8 | 9 | def status_weights 10 | {:success => 1, :failure => 2, :error => 3, :pending => 4, :disabled => 5} 11 | end 12 | end 13 | 14 | attr_reader :attributes 15 | attr_accessor :source_location 16 | def initialize(attributes={}) 17 | @outcome = nil 18 | @attributes = attributes 19 | end 20 | 21 | Attest::ExpectationResult.status_types.each do |status| 22 | eval <<-EOT 23 | def #{status} 24 | @outcome = current_method 25 | end 26 | def #{status}? 27 | current_method.chop == @outcome 28 | end 29 | EOT 30 | end 31 | 32 | def status 33 | @outcome 34 | end 35 | 36 | def update(attributes={}) 37 | @attributes.merge!(attributes) 38 | end 39 | 40 | def status_weight 41 | Attest::ExpectationResult.status_weights[status.to_sym] 42 | end 43 | 44 | def <=>(another_result) 45 | return 1 unless another_result 46 | self.status_weight <=> another_result.status_weight 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/attest/interface/output_writer_configurator.rb: -------------------------------------------------------------------------------- 1 | require 'attest/output/basic_output_writer' 2 | require 'attest/output/test_unit_output_writer' 3 | require 'attest/output/failures_only_output_writer' 4 | 5 | module Attest 6 | class OutputWriterConfigurator 7 | class << self 8 | def configure(output_writer_identifier) 9 | output_writer_identifier = output_writer_identifier || default_output_writer_identifier 10 | raise "You have specified an unknown output writer" unless output_writer_identifiers.include? output_writer_identifier 11 | output_writer_class = "#{output_writer_identifier}OutputWriter" 12 | #Attest.config.output_writer = Attest::Output.const_get(output_writer_class).new 13 | Attest::Output.const_get(output_writer_class).new 14 | end 15 | 16 | def default_output_writer_identifier 17 | "Basic" 18 | end 19 | 20 | def output_writer_identifiers 21 | [default_output_writer_identifier, "TestUnit", "FailuresOnly"] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/attest/interface/possible_tests_configurator.rb: -------------------------------------------------------------------------------- 1 | module Attest 2 | class PossibleTestsConfigurator 3 | class << self 4 | def configure(included_locations, excluded_locations = nil) 5 | raise "Need to know location for tests" if included_locations.compact.size == 0 6 | possible_test_files = included_test_files included_locations 7 | files_to_ignore = excluded_test_files excluded_locations 8 | possible_test_files - files_to_ignore 9 | end 10 | 11 | def included_test_files(included_locations) 12 | file_list_from_list_of included_locations 13 | end 14 | 15 | def excluded_test_files(excluded_locations) 16 | return [] if excluded_locations.nil? 17 | file_list_from_list_of excluded_locations 18 | end 19 | 20 | def file_list_from_list_of(locations) 21 | file_list = [] 22 | locations.compact.each do |location| 23 | expanded_location = File.expand_path(location) 24 | file_list << file_list_from_single(expanded_location) 25 | end 26 | file_list.flatten 27 | end 28 | 29 | def file_list_from_single(location) 30 | return location if File.file? location 31 | Dir[File.join(File.expand_path(location), "**/*.rb")].collect { |ruby_file| ruby_file } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/attest/interface/test_double_configurator.rb: -------------------------------------------------------------------------------- 1 | require 'attest/execution_context' 2 | 3 | module Attest 4 | class TestDoubleConfigurator 5 | class << self 6 | def configure(test_double_identifier) 7 | test_double_identifier = test_double_identifier || default_test_double_identifier 8 | raise "You have specified an unsupported test double framework" unless test_double_identifiers.include? test_double_identifier 9 | self.send(:"configure_#{test_double_identifier}") 10 | #Attest.config.testdouble = test_double_identifier 11 | test_double_identifier 12 | end 13 | 14 | def configure_mocha 15 | begin 16 | #how would this work when bundler is in play 17 | require "mocha_standalone" 18 | rescue LoadError => e 19 | puts "Trying to use mocha for test double functionality, but can't find it!" 20 | puts "Perhaps you forgot to install the mocha gem." 21 | exit 22 | end 23 | Attest::ExecutionContext.class_eval do 24 | include Mocha::API # need this so that methods like stub() and mock() can be accessed directly from the execution context 25 | end 26 | end 27 | 28 | def configure_none 29 | end 30 | 31 | def default_test_double_identifier 32 | "mocha" 33 | end 34 | 35 | def test_double_identifiers 36 | [default_test_double_identifier, "none"] 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/attest/output/basic_output_writer.rb: -------------------------------------------------------------------------------- 1 | require 'attest/output/output_writer' 2 | require 'attest/expectation_result' 3 | 4 | module Attest 5 | module Output 6 | class BasicOutputWriter < Attest::Output::OutputWriter 7 | def before_container(container) 8 | previous_container = @containers.last 9 | @containers << container 10 | puts "#{container.file}:" unless previous_container && previous_container.file == container.file 11 | puts " #{ container.description }" 12 | end 13 | 14 | def after_container(container) 15 | puts 16 | end 17 | 18 | def before_test(test_object) 19 | print " - #{test_object.description}" 20 | end 21 | 22 | def after_test(test_object) 23 | relevant_result = determine_relevant_result test_object 24 | print " [#{relevant_result.status.upcase}]" if relevant_result 25 | if relevant_result && relevant_result.failure? 26 | 2.times { puts } 27 | puts " #{relevant_result.source_location}" 28 | elsif relevant_result && relevant_result.error? 29 | e = relevant_result.attributes[:unexpected_error] 30 | 2.times { puts } 31 | puts " #{e.class}: #{e.message}" 32 | e.backtrace.each do |line| 33 | break if line =~ /lib\/attest/ 34 | puts " #{line} " 35 | end 36 | end 37 | puts 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/attest/output/failures_only_output_writer.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require 'attest/output/output_writer' 3 | require 'attest/expectation_result' 4 | 5 | module Attest 6 | module Output 7 | class FailuresOnlyOutputWriter < Attest::Output::OutputWriter 8 | def initialize 9 | super() 10 | @relevant_outputs = StringIO.new 11 | end 12 | 13 | def after_all_tests 14 | super 15 | @relevant_outputs.rewind 16 | 2.times {puts} 17 | puts @relevant_outputs.readlines 18 | end 19 | 20 | def before_container(container) 21 | previous_container = @containers.last 22 | @containers << container 23 | end 24 | 25 | def after_test(test_object) 26 | relevant_result = determine_relevant_result test_object 27 | if relevant_result && relevant_result.failure? 28 | @relevant_outputs.puts "#{@containers.last.file}" 29 | @relevant_outputs.puts " #{@containers.last.description}" 30 | @relevant_outputs.puts " - #{test_object.description} [#{relevant_result.status.upcase}]" 31 | @relevant_outputs.puts " #{relevant_result.source_location}" 32 | @relevant_outputs.puts 33 | elsif relevant_result && relevant_result.error? 34 | e = relevant_result.attributes[:unexpected_error] 35 | @relevant_outputs.puts "#{@containers.last.file}" 36 | @relevant_outputs.puts " #{@containers.last.description}" 37 | @relevant_outputs.puts " - #{test_object.description} [#{relevant_result.status.upcase}]" 38 | @relevant_outputs.puts " #{e.class}: #{e.message}" 39 | e.backtrace.each do |line| 40 | @relevant_outputs.puts " #{line} " 41 | end 42 | @relevant_outputs.puts 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/attest/output/output_writer.rb: -------------------------------------------------------------------------------- 1 | require 'attest/output/output_writer_interface' 2 | require 'attest/expectation_result' 3 | 4 | module Attest 5 | module Output 6 | class OutputWriter 7 | include OutputWriterInterface 8 | def initialize 9 | self.instance_variable_set("@containers", []) 10 | self.instance_variable_set("@start_time", nil) 11 | self.instance_variable_set("@end_time", nil) 12 | end 13 | 14 | def before_all_tests 15 | @start_time = Time.now 16 | end 17 | def after_all_tests 18 | @end_time = Time.now 19 | end 20 | def before_container(container) 21 | end 22 | def after_container(container) 23 | end 24 | def before_test(test_object) 25 | end 26 | def after_test(test_object) 27 | end 28 | def summary 29 | return unless @containers.size >= 1 30 | expectation_status_hash = blank_status_hash 31 | overall_test_status_hash = blank_status_hash 32 | test_count = 0 33 | @containers.each do |container| 34 | container.test_objects.each do |test_object| 35 | test_count += 1 36 | current_test_statuses = determine_test_status test_object 37 | overall_test_status_hash = merge_counting_hashes(overall_test_status_hash, current_test_statuses[0]) 38 | expectation_status_hash = merge_counting_hashes(expectation_status_hash, current_test_statuses[1]) 39 | end 40 | end 41 | puts 42 | print "#{test_count} tests #{expectation_status_hash.inject(0){|sum, tuple| sum + tuple[1]}} expectations" 43 | Attest::ExpectationResult.status_weights.sort{|a, b| a[1] <=> b[1]}.each {|status, weight| print " #{expectation_status_hash[status]} #{status.to_s}"} 44 | puts 45 | puts "Finished in #{elapsed_time(@end_time, @start_time)}" 46 | end 47 | def ignore_container(container) 48 | if @containers.last.object_id == container.object_id 49 | @containers.delete @containers.last 50 | else 51 | @containers.delete_if {|current_container| current_container.object_id == container.object_id} 52 | end 53 | end 54 | def an_error(error_object) 55 | puts "#{error_object.class}: #{error_object.message}" 56 | error_object.backtrace.each do |line| 57 | puts " #{line} " 58 | end 59 | end 60 | 61 | private 62 | def elapsed_time(end_time, start_time) 63 | units = ["milliseconds", "seconds", "minutes", "hours"] 64 | elapsed_seconds = end_time - start_time 65 | if elapsed_seconds < 1 66 | elapsed_time_as_string = "#{round_to(2, (elapsed_seconds * 1000))} #{units[0]}" 67 | elsif elapsed_seconds >= 1 && elapsed_seconds < 60 68 | elapsed_time_as_string = "#{round_to(2, elapsed_seconds)} #{units[1]}" 69 | elsif elapsed_seconds >= 60 && elapsed_seconds < 3600 70 | minsec = elapsed_seconds.divmod(60).collect{|num| round_to(2, num)} 71 | elapsed_time_as_string = "#{minsec[0]} #{units[2]}, #{minsec[1]} #{units[1]}" 72 | else 73 | minsec = elapsed_seconds.divmod(60).collect{|num| round_to(2, num)} 74 | hourminsec = minsec[0].divmod(60).collect{|num| round_to(2, num)} 75 | hourminsec << minsec[1] 76 | elapsed_time_as_string = "#{hourminsec[0]} #{units[3]}, #{hourminsec[1]} #{units[2]}, #{hourminsec[2]} #{units[1]}" 77 | end 78 | elapsed_time_as_string 79 | end 80 | 81 | def round_to(decimal_places, number) 82 | rounded = (number * 10**decimal_places).round.to_f / 10**decimal_places 83 | rounded_as_int = (rounded == rounded.to_i ? rounded.to_i : rounded) 84 | rounded_as_int 85 | end 86 | 87 | def determine_relevant_result(test_object) 88 | relevant_result = nil 89 | test_object.results.each do |result| 90 | relevant_result = result unless result.success? 91 | end 92 | relevant_result 93 | end 94 | 95 | def determine_test_status(test_object) 96 | expectation_status_hash = blank_status_hash 97 | overall_test_status_hash = blank_status_hash 98 | dominant_result = nil 99 | test_object.results.each do |result| 100 | expectation_status_hash[result.status.to_sym] += 1 101 | dominant_result = result if result > dominant_result 102 | end 103 | raise "Unexpected result status encountered! WTF!!!" if expectation_status_hash.keys.size > Attest::ExpectationResult.status_types.size 104 | raise "Test without status encountered, all test should have a status!" unless dominant_result 105 | overall_test_status_hash[dominant_result.status.to_sym] += 1 106 | [overall_test_status_hash, expectation_status_hash] 107 | end 108 | 109 | def merge_counting_hashes(hash1, hash2) 110 | hash1.inject(hash2) do |accumulator_hash, tuple| 111 | accumulator_hash[tuple[0]] += tuple[1] 112 | accumulator_hash 113 | end 114 | end 115 | 116 | def blank_status_hash 117 | Attest::ExpectationResult.status_types.inject({}) do |accumulator, status| 118 | accumulator[status] = 0 119 | accumulator 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/attest/output/output_writer_interface.rb: -------------------------------------------------------------------------------- 1 | module Attest 2 | module Output 3 | module OutputWriterInterface 4 | def before_all_tests 5 | end 6 | def after_all_tests 7 | end 8 | def before_container(container) 9 | end 10 | def after_container(container) 11 | end 12 | def before_test(test_object) 13 | end 14 | def after_test(test_object) 15 | end 16 | def summary 17 | end 18 | def ignore_container(container) 19 | end 20 | def an_error(error_object) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/attest/output/test_unit_output_writer.rb: -------------------------------------------------------------------------------- 1 | require 'attest/output/output_writer' 2 | require 'attest/expectation_result' 3 | 4 | module Attest 5 | module Output 6 | class TestUnitOutputWriter < Attest::Output::OutputWriter 7 | def before_all_tests 8 | super 9 | puts 10 | end 11 | 12 | def after_all_tests 13 | super 14 | puts 15 | end 16 | 17 | def before_container(container) 18 | previous_container = @containers.last 19 | @containers << container 20 | end 21 | 22 | def after_test(test_object) 23 | relevant_result = determine_relevant_result test_object 24 | if relevant_result 25 | print "#{relevant_result.status.upcase[0]}" 26 | else 27 | print '.' 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/attest/proc/proc_source_reader.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require 'irb/ruby-lex' 3 | 4 | module Attest 5 | class ProcSourceReader 6 | def initialize(file, line) 7 | @file = file 8 | @start_line = line 9 | end 10 | 11 | def self.find(file, line) 12 | source_reader = ProcSourceReader.new(file, line) 13 | source_reader.read_source 14 | 15 | end 16 | 17 | def read_source 18 | lines_starting_with_proc = read_lines_from_file 19 | return nil if lines_starting_with_proc.nil? 20 | lexer = RubyLex.new 21 | lexer.set_input(StringIO.new(lines_starting_with_proc.join)) 22 | start_token, end_token = nil, nil 23 | nesting = 0 24 | while token = lexer.token 25 | if RubyToken::TkDO === token || RubyToken::TkfLBRACE === token 26 | nesting += 1 27 | start_token = token if nesting == 1 28 | elsif RubyToken::TkEND === token || RubyToken::TkRBRACE === token 29 | if nesting == 1 30 | end_token = token 31 | break 32 | end 33 | nesting -= 1 34 | end 35 | end 36 | proc_lines = lines_starting_with_proc[start_token.line_no - 1 .. end_token.line_no - 1] 37 | proc_lines 38 | end 39 | 40 | def read_lines_from_file 41 | raise "No file for proc where does it come from" unless @file 42 | begin 43 | File.readlines(@file)[(@start_line - 1) .. -1] 44 | rescue 45 | nil 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/attest/rake/attesttask.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/tasklib' 3 | 4 | module Rake 5 | class AttestTask < TaskLib 6 | attr_accessor :include, :exclude, :outputwriter, :testdouble 7 | def initialize 8 | @include = "attest/" 9 | @exclude = nil 10 | @outputwriter = "Basic" 11 | @testdouble = "mocha" 12 | yield self if block_given? 13 | define 14 | end 15 | 16 | def define 17 | desc "Run attest tests" 18 | task :attest do 19 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/'))) unless $:.include?(File.expand_path(File.join(File.dirname(__FILE__), '../../../lib'))) 20 | require 'attest' 21 | require 'attest/interface/output_writer_configurator' 22 | require 'attest/interface/test_double_configurator' 23 | require 'attest/interface/possible_tests_configurator' 24 | 25 | Attest.configure do |config| 26 | config.output_writer = Attest::OutputWriterConfigurator.configure(@outputwriter) 27 | config.testdouble = Attest::TestDoubleConfigurator.configure(@testdouble) 28 | config.possible_tests = Attest::PossibleTestsConfigurator.configure(@include, @exclude) 29 | end 30 | 31 | require 'attest/test_loader' 32 | 33 | Attest::TestLoader.execute(Attest.config.possible_tests, Attest.config.output_writer) 34 | end 35 | self 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/attest/test_container.rb: -------------------------------------------------------------------------------- 1 | require 'attest' 2 | 3 | module Attest 4 | class TestContainer 5 | 6 | attr_reader :description, :test_objects, :file 7 | attr_accessor :before, :after 8 | 9 | def initialize(description) 10 | @file = Attest.current_file 11 | @description = description 12 | @test_objects = [] 13 | end 14 | 15 | def add(test) 16 | @test_objects << test 17 | end 18 | 19 | def execute_all 20 | Attest.output_writer.before_container(self) 21 | container_context = Attest::ExecutionContext.new 22 | begin 23 | container_context.instance_eval(&@before) if @before 24 | @test_objects.each do |test_object| 25 | test_object.run container_context 26 | end 27 | container_context.instance_eval(&@after) if @after 28 | rescue => e 29 | Attest.output_writer.an_error(e) 30 | Attest.output_writer.ignore_container(self) 31 | end 32 | Attest.output_writer.after_container(self) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/attest/test_loader.rb: -------------------------------------------------------------------------------- 1 | require 'attest' 2 | require 'attest/core_ext/kernel' 3 | 4 | module Attest 5 | class TestLoader 6 | class << self 7 | def execute(possible_tests, output_writer) 8 | switch_on_attest_mode 9 | output_writer.before_all_tests 10 | possible_tests.each do |ruby_file| 11 | Attest.config.current_file = ruby_file 12 | load ruby_file 13 | end 14 | output_writer.after_all_tests 15 | output_writer.summary 16 | switch_off_attest_mode 17 | end 18 | 19 | def switch_on_attest_mode 20 | ENV["attest"] = "true" 21 | end 22 | 23 | def switch_off_attest_mode 24 | ENV["attest"] = nil 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/attest/test_object.rb: -------------------------------------------------------------------------------- 1 | require 'attest' 2 | require 'attest/execution_context' 3 | require 'attest/expectation_result' 4 | 5 | module Attest 6 | class TestObject 7 | attr_reader :description, :results 8 | attr_accessor :nosetup, :disabled, :before, :after 9 | def initialize(description, test_block) 10 | @description = description 11 | @test_block = test_block 12 | @results = [] 13 | end 14 | 15 | def run(persistent_context) 16 | Attest.output_writer.before_test(self) 17 | error = nil 18 | context = Attest::ExecutionContext.new(persistent_context) 19 | begin 20 | #Object.class_eval do 21 | #define_method :itself do 22 | #subject = self 23 | #context.instance_eval {@subject = subject} 24 | #context 25 | #end 26 | #end 27 | context.instance_eval(&@before) if @before && !nosetup && !disabled 28 | context.instance_eval(&@test_block) if @test_block && !disabled 29 | context.instance_eval(&@after) if @after && !nosetup && !disabled 30 | rescue => e 31 | error = e 32 | ensure 33 | @results = context.results 34 | add_unexpected_error_result(error) if error 35 | add_pending_result unless @test_block 36 | add_disabled_result if disabled 37 | add_success_result if @results.size == 0 38 | end 39 | Attest.output_writer.after_test(self) 40 | end 41 | 42 | private 43 | def add_unexpected_error_result(error) 44 | create_and_add_result(:unexpected_error => error) {|result| result.error} 45 | end 46 | 47 | def add_pending_result 48 | create_and_add_result{|result| result.pending} 49 | end 50 | 51 | def add_disabled_result 52 | create_and_add_result{|result| result.disabled} 53 | end 54 | 55 | def add_success_result 56 | create_and_add_result{|result| result.success} 57 | end 58 | 59 | def create_and_add_result(opts={}) 60 | result = Attest::ExpectationResult.new(opts) 61 | yield result 62 | @results << result 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/attest/test_parser.rb: -------------------------------------------------------------------------------- 1 | require 'attest/test_container' 2 | require 'attest/execution_context' 3 | require 'attest/test_object' 4 | require 'attest/core_ext/proc' 5 | 6 | module Attest 7 | class TestParser 8 | def initialize(description, block) 9 | @description = description 10 | @block = block 11 | @before_all = nil 12 | @after_all = nil 13 | @before = nil 14 | @after = nil 15 | @tests = {} 16 | @nosetup_tests = {} 17 | @disabled_tests = {} 18 | @freestyle_tests = [] 19 | end 20 | 21 | def parse 22 | self.instance_eval(&@block) 23 | test_container = Attest::TestContainer.new(@description) 24 | build_test_objects_and_add_to_container test_container 25 | build_freestyle_test_objects_and_add_to test_container 26 | test_container 27 | end 28 | 29 | def before_all(&block) 30 | @before_all = block 31 | end 32 | 33 | def after_all(&block) 34 | @after_all = block 35 | end 36 | 37 | def before_each(&block) 38 | @before = block 39 | end 40 | 41 | def after_each(&block) 42 | @after = block 43 | end 44 | 45 | def test(description, &block) 46 | if @next_test_without_setup 47 | @nosetup_tests[description] = true 48 | end 49 | if @next_test_disabled 50 | @disabled_tests[description] = true 51 | end 52 | @tests[description] = block 53 | @next_test_without_setup = false 54 | @next_test_disabled = false 55 | end 56 | 57 | def nosetup 58 | @next_test_without_setup = true 59 | end 60 | 61 | def disabled 62 | @next_test_disabled = true 63 | end 64 | 65 | def method_missing(name, *args, &block) 66 | unless Attest::ExecutionContext.assertions.include? name 67 | super 68 | end 69 | @freestyle_tests << {:method_name => name, :args => args, :block => block} 70 | end 71 | 72 | private 73 | def build_test_objects_and_add_to_container(test_container) 74 | test_container.before = @before_all 75 | test_container.after = @after_all 76 | @tests.each_pair do |description, test_block| 77 | test_object = TestObject.new(description, test_block) 78 | test_object.nosetup = true if @nosetup_tests[description] 79 | test_object.disabled = true if @disabled_tests[description] 80 | test_object.before = @before 81 | test_object.after = @after 82 | test_container.add(test_object) 83 | end 84 | end 85 | 86 | def build_freestyle_test_objects_and_add_to(test_container) 87 | @freestyle_tests.each_with_index do |assertion_info, index| 88 | test_block = lambda {send(assertion_info[:method_name], *assertion_info[:args], &assertion_info[:block])} 89 | block_code = assertion_info[:block].to_string.collect{|line| line.strip} 90 | test_object = TestObject.new("freestyle test #{index + 1} - #{block_code.join(' ')}", test_block) 91 | test_object.nosetup = false 92 | test_container.add(test_object) 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/trollop.rb: -------------------------------------------------------------------------------- 1 | ## lib/trollop.rb -- trollop command-line processing library 2 | ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net) 3 | ## Copyright:: Copyright 2007 William Morgan 4 | ## License:: the same terms as ruby itself 5 | 6 | require 'date' 7 | 8 | module Trollop 9 | 10 | VERSION = "1.16.2" 11 | 12 | ## Thrown by Parser in the event of a commandline error. Not needed if 13 | ## you're using the Trollop::options entry. 14 | class CommandlineError < StandardError; end 15 | 16 | ## Thrown by Parser if the user passes in '-h' or '--help'. Handled 17 | ## automatically by Trollop#options. 18 | class HelpNeeded < StandardError; end 19 | 20 | ## Thrown by Parser if the user passes in '-h' or '--version'. Handled 21 | ## automatically by Trollop#options. 22 | class VersionNeeded < StandardError; end 23 | 24 | ## Regex for floating point numbers 25 | FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/ 26 | 27 | ## Regex for parameters 28 | PARAM_RE = /^-(-|\.$|[^\d\.])/ 29 | 30 | ## The commandline parser. In typical usage, the methods in this class 31 | ## will be handled internally by Trollop::options. In this case, only the 32 | ## #opt, #banner and #version, #depends, and #conflicts methods will 33 | ## typically be called. 34 | ## 35 | ## If you want to instantiate this class yourself (for more complicated 36 | ## argument-parsing logic), call #parse to actually produce the output hash, 37 | ## and consider calling it from within 38 | ## Trollop::with_standard_exception_handling. 39 | class Parser 40 | 41 | ## The set of values that indicate a flag option when passed as the 42 | ## +:type+ parameter of #opt. 43 | FLAG_TYPES = [:flag, :bool, :boolean] 44 | 45 | ## The set of values that indicate a single-parameter (normal) option when 46 | ## passed as the +:type+ parameter of #opt. 47 | ## 48 | ## A value of +io+ corresponds to a readable IO resource, including 49 | ## a filename, URI, or the strings 'stdin' or '-'. 50 | SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date] 51 | 52 | ## The set of values that indicate a multiple-parameter option (i.e., that 53 | ## takes multiple space-separated values on the commandline) when passed as 54 | ## the +:type+ parameter of #opt. 55 | MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates] 56 | 57 | ## The complete set of legal values for the +:type+ parameter of #opt. 58 | TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES 59 | 60 | INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc: 61 | 62 | ## The values from the commandline that were not interpreted by #parse. 63 | attr_reader :leftovers 64 | 65 | ## The complete configuration hashes for each option. (Mainly useful 66 | ## for testing.) 67 | attr_reader :specs 68 | 69 | ## Initializes the parser, and instance-evaluates any block given. 70 | def initialize *a, &b 71 | @version = nil 72 | @leftovers = [] 73 | @specs = {} 74 | @long = {} 75 | @short = {} 76 | @order = [] 77 | @constraints = [] 78 | @stop_words = [] 79 | @stop_on_unknown = false 80 | 81 | #instance_eval(&b) if b # can't take arguments 82 | cloaker(&b).bind(self).call(*a) if b 83 | end 84 | 85 | ## Define an option. +name+ is the option name, a unique identifier 86 | ## for the option that you will use internally, which should be a 87 | ## symbol or a string. +desc+ is a string description which will be 88 | ## displayed in help messages. 89 | ## 90 | ## Takes the following optional arguments: 91 | ## 92 | ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s. 93 | ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+. 94 | ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given. 95 | ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+. 96 | ## [+:required+] If set to +true+, the argument must be provided on the commandline. 97 | ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.) 98 | ## 99 | ## Note that there are two types of argument multiplicity: an argument 100 | ## can take multiple values, e.g. "--arg 1 2 3". An argument can also 101 | ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2". 102 | ## 103 | ## Arguments that take multiple values should have a +:type+ parameter 104 | ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+ 105 | ## value of an array of the correct type (e.g. [String]). The 106 | ## value of this argument will be an array of the parameters on the 107 | ## commandline. 108 | ## 109 | ## Arguments that can occur multiple times should be marked with 110 | ## +:multi+ => +true+. The value of this argument will also be an array. 111 | ## In contrast with regular non-multi options, if not specified on 112 | ## the commandline, the default value will be [], not nil. 113 | ## 114 | ## These two attributes can be combined (e.g. +:type+ => +:strings+, 115 | ## +:multi+ => +true+), in which case the value of the argument will be 116 | ## an array of arrays. 117 | ## 118 | ## There's one ambiguous case to be aware of: when +:multi+: is true and a 119 | ## +:default+ is set to an array (of something), it's ambiguous whether this 120 | ## is a multi-value argument as well as a multi-occurrence argument. 121 | ## In thise case, Trollop assumes that it's not a multi-value argument. 122 | ## If you want a multi-value, multi-occurrence argument with a default 123 | ## value, you must specify +:type+ as well. 124 | 125 | def opt name, desc="", opts={} 126 | raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name 127 | 128 | ## fill in :type 129 | opts[:type] = # normalize 130 | case opts[:type] 131 | when :boolean, :bool; :flag 132 | when :integer; :int 133 | when :integers; :ints 134 | when :double; :float 135 | when :doubles; :floats 136 | when Class 137 | case opts[:type].name 138 | when 'TrueClass', 'FalseClass'; :flag 139 | when 'String'; :string 140 | when 'Integer'; :int 141 | when 'Float'; :float 142 | when 'IO'; :io 143 | when 'Date'; :date 144 | else 145 | raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'" 146 | end 147 | when nil; nil 148 | else 149 | raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type]) 150 | opts[:type] 151 | end 152 | 153 | ## for options with :multi => true, an array default doesn't imply 154 | ## a multi-valued argument. for that you have to specify a :type 155 | ## as well. (this is how we disambiguate an ambiguous situation; 156 | ## see the docs for Parser#opt for details.) 157 | disambiguated_default = 158 | if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type] 159 | opts[:default].first 160 | else 161 | opts[:default] 162 | end 163 | 164 | type_from_default = 165 | case disambiguated_default 166 | when Integer; :int 167 | when Numeric; :float 168 | when TrueClass, FalseClass; :flag 169 | when String; :string 170 | when IO; :io 171 | when Date; :date 172 | when Array 173 | if opts[:default].empty? 174 | raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'" 175 | end 176 | case opts[:default][0] # the first element determines the types 177 | when Integer; :ints 178 | when Numeric; :floats 179 | when String; :strings 180 | when IO; :ios 181 | when Date; :dates 182 | else 183 | raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'" 184 | end 185 | when nil; nil 186 | else 187 | raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'" 188 | end 189 | 190 | raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default 191 | 192 | opts[:type] = opts[:type] || type_from_default || :flag 193 | 194 | ## fill in :long 195 | opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-") 196 | opts[:long] = 197 | case opts[:long] 198 | when /^--([^-].*)$/ 199 | $1 200 | when /^[^-]/ 201 | opts[:long] 202 | else 203 | raise ArgumentError, "invalid long option name #{opts[:long].inspect}" 204 | end 205 | raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]] 206 | 207 | ## fill in :short 208 | opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none 209 | opts[:short] = case opts[:short] 210 | when /^-(.)$/; $1 211 | when nil, :none, /^.$/; opts[:short] 212 | else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'" 213 | end 214 | 215 | if opts[:short] 216 | raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]] 217 | raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX 218 | end 219 | 220 | ## fill in :default for flags 221 | opts[:default] = false if opts[:type] == :flag && opts[:default].nil? 222 | 223 | ## autobox :default for :multi (multi-occurrence) arguments 224 | opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array) 225 | 226 | ## fill in :multi 227 | opts[:multi] ||= false 228 | 229 | opts[:desc] ||= desc 230 | @long[opts[:long]] = name 231 | @short[opts[:short]] = name if opts[:short] && opts[:short] != :none 232 | @specs[name] = opts 233 | @order << [:opt, name] 234 | end 235 | 236 | ## Sets the version string. If set, the user can request the version 237 | ## on the commandline. Should probably be of the form " 238 | ## ". 239 | def version s=nil; @version = s if s; @version end 240 | 241 | ## Adds text to the help display. Can be interspersed with calls to 242 | ## #opt to build a multi-section help page. 243 | def banner s; @order << [:text, s] end 244 | alias :text :banner 245 | 246 | ## Marks two (or more!) options as requiring each other. Only handles 247 | ## undirected (i.e., mutual) dependencies. Directed dependencies are 248 | ## better modeled with Trollop::die. 249 | def depends *syms 250 | syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } 251 | @constraints << [:depends, syms] 252 | end 253 | 254 | ## Marks two (or more!) options as conflicting. 255 | def conflicts *syms 256 | syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } 257 | @constraints << [:conflicts, syms] 258 | end 259 | 260 | ## Defines a set of words which cause parsing to terminate when 261 | ## encountered, such that any options to the left of the word are 262 | ## parsed as usual, and options to the right of the word are left 263 | ## intact. 264 | ## 265 | ## A typical use case would be for subcommand support, where these 266 | ## would be set to the list of subcommands. A subsequent Trollop 267 | ## invocation would then be used to parse subcommand options, after 268 | ## shifting the subcommand off of ARGV. 269 | def stop_on *words 270 | @stop_words = [*words].flatten 271 | end 272 | 273 | ## Similar to #stop_on, but stops on any unknown word when encountered 274 | ## (unless it is a parameter for an argument). This is useful for 275 | ## cases where you don't know the set of subcommands ahead of time, 276 | ## i.e., without first parsing the global options. 277 | def stop_on_unknown 278 | @stop_on_unknown = true 279 | end 280 | 281 | ## Parses the commandline. Typically called by Trollop::options, 282 | ## but you can call it directly if you need more control. 283 | ## 284 | ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions. 285 | def parse cmdline=ARGV 286 | vals = {} 287 | required = {} 288 | 289 | opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"] 290 | opt :help, "Show this message" unless @specs[:help] || @long["help"] 291 | 292 | @specs.each do |sym, opts| 293 | required[sym] = true if opts[:required] 294 | vals[sym] = opts[:default] 295 | vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil 296 | end 297 | 298 | resolve_default_short_options 299 | 300 | ## resolve symbols 301 | given_args = {} 302 | @leftovers = each_arg cmdline do |arg, params| 303 | sym = case arg 304 | when /^-([^-])$/ 305 | @short[$1] 306 | when /^--([^-]\S*)$/ 307 | @long[$1] 308 | else 309 | raise CommandlineError, "invalid argument syntax: '#{arg}'" 310 | end 311 | raise CommandlineError, "unknown argument '#{arg}'" unless sym 312 | 313 | if given_args.include?(sym) && !@specs[sym][:multi] 314 | raise CommandlineError, "option '#{arg}' specified multiple times" 315 | end 316 | 317 | given_args[sym] ||= {} 318 | 319 | given_args[sym][:arg] = arg 320 | given_args[sym][:params] ||= [] 321 | 322 | # The block returns the number of parameters taken. 323 | num_params_taken = 0 324 | 325 | unless params.nil? 326 | if SINGLE_ARG_TYPES.include?(@specs[sym][:type]) 327 | given_args[sym][:params] << params[0, 1] # take the first parameter 328 | num_params_taken = 1 329 | elsif MULTI_ARG_TYPES.include?(@specs[sym][:type]) 330 | given_args[sym][:params] << params # take all the parameters 331 | num_params_taken = params.size 332 | end 333 | end 334 | 335 | num_params_taken 336 | end 337 | 338 | ## check for version and help args 339 | raise VersionNeeded if given_args.include? :version 340 | raise HelpNeeded if given_args.include? :help 341 | 342 | ## check constraint satisfaction 343 | @constraints.each do |type, syms| 344 | constraint_sym = syms.find { |sym| given_args[sym] } 345 | next unless constraint_sym 346 | 347 | case type 348 | when :depends 349 | syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym } 350 | when :conflicts 351 | syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) } 352 | end 353 | end 354 | 355 | required.each do |sym, val| 356 | raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym 357 | end 358 | 359 | ## parse parameters 360 | given_args.each do |sym, given_data| 361 | arg = given_data[:arg] 362 | params = given_data[:params] 363 | 364 | opts = @specs[sym] 365 | raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag 366 | 367 | vals["#{sym}_given".intern] = true # mark argument as specified on the commandline 368 | 369 | case opts[:type] 370 | when :flag 371 | vals[sym] = !opts[:default] 372 | when :int, :ints 373 | vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } } 374 | when :float, :floats 375 | vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } } 376 | when :string, :strings 377 | vals[sym] = params.map { |pg| pg.map { |p| p.to_s } } 378 | when :io, :ios 379 | vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } } 380 | when :date, :dates 381 | vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } } 382 | end 383 | 384 | if SINGLE_ARG_TYPES.include?(opts[:type]) 385 | unless opts[:multi] # single parameter 386 | vals[sym] = vals[sym][0][0] 387 | else # multiple options, each with a single parameter 388 | vals[sym] = vals[sym].map { |p| p[0] } 389 | end 390 | elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi] 391 | vals[sym] = vals[sym][0] # single option, with multiple parameters 392 | end 393 | # else: multiple options, with multiple parameters 394 | end 395 | 396 | ## modify input in place with only those 397 | ## arguments we didn't process 398 | cmdline.clear 399 | @leftovers.each { |l| cmdline << l } 400 | 401 | ## allow openstruct-style accessors 402 | class << vals 403 | def method_missing(m, *args) 404 | self[m] || self[m.to_s] 405 | end 406 | end 407 | vals 408 | end 409 | 410 | def parse_date_parameter param, arg #:nodoc: 411 | begin 412 | begin 413 | time = Chronic.parse(param) 414 | rescue NameError 415 | # chronic is not available 416 | end 417 | time ? Date.new(time.year, time.month, time.day) : Date.parse(param) 418 | rescue ArgumentError => e 419 | raise CommandlineError, "option '#{arg}' needs a date" 420 | end 421 | end 422 | 423 | ## Print the help message to +stream+. 424 | def educate stream=$stdout 425 | width # just calculate it now; otherwise we have to be careful not to 426 | # call this unless the cursor's at the beginning of a line. 427 | 428 | left = {} 429 | @specs.each do |name, spec| 430 | left[name] = "--#{spec[:long]}" + 431 | (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") + 432 | case spec[:type] 433 | when :flag; "" 434 | when :int; " " 435 | when :ints; " " 436 | when :string; " " 437 | when :strings; " " 438 | when :float; " " 439 | when :floats; " " 440 | when :io; " " 441 | when :ios; " " 442 | when :date; " " 443 | when :dates; " " 444 | end 445 | end 446 | 447 | leftcol_width = left.values.map { |s| s.length }.max || 0 448 | rightcol_start = leftcol_width + 6 # spaces 449 | 450 | unless @order.size > 0 && @order.first.first == :text 451 | stream.puts "#@version\n" if @version 452 | stream.puts "Options:" 453 | end 454 | 455 | @order.each do |what, opt| 456 | if what == :text 457 | stream.puts wrap(opt) 458 | next 459 | end 460 | 461 | spec = @specs[opt] 462 | stream.printf " %#{leftcol_width}s: ", left[opt] 463 | desc = spec[:desc] + begin 464 | default_s = case spec[:default] 465 | when $stdout; "" 466 | when $stdin; "" 467 | when $stderr; "" 468 | when Array 469 | spec[:default].join(", ") 470 | else 471 | spec[:default].to_s 472 | end 473 | 474 | if spec[:default] 475 | if spec[:desc] =~ /\.$/ 476 | " (Default: #{default_s})" 477 | else 478 | " (default: #{default_s})" 479 | end 480 | else 481 | "" 482 | end 483 | end 484 | stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start) 485 | end 486 | end 487 | 488 | def width #:nodoc: 489 | @width ||= if $stdout.tty? 490 | begin 491 | require 'curses' 492 | Curses::init_screen 493 | x = Curses::cols 494 | Curses::close_screen 495 | x 496 | rescue Exception 497 | 80 498 | end 499 | else 500 | 80 501 | end 502 | end 503 | 504 | def wrap str, opts={} # :nodoc: 505 | if str == "" 506 | [""] 507 | else 508 | str.split("\n").map { |s| wrap_line s, opts }.flatten 509 | end 510 | end 511 | 512 | ## The per-parser version of Trollop::die (see that for documentation). 513 | def die arg, msg 514 | if msg 515 | $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}." 516 | else 517 | $stderr.puts "Error: #{arg}." 518 | end 519 | $stderr.puts "Try --help for help." 520 | exit(-1) 521 | end 522 | 523 | private 524 | 525 | ## yield successive arg, parameter pairs 526 | def each_arg args 527 | remains = [] 528 | i = 0 529 | 530 | until i >= args.length 531 | if @stop_words.member? args[i] 532 | remains += args[i .. -1] 533 | return remains 534 | end 535 | case args[i] 536 | when /^--$/ # arg terminator 537 | remains += args[(i + 1) .. -1] 538 | return remains 539 | when /^--(\S+?)=(.*)$/ # long argument with equals 540 | yield "--#{$1}", [$2] 541 | i += 1 542 | when /^--(\S+)$/ # long argument 543 | params = collect_argument_parameters(args, i + 1) 544 | unless params.empty? 545 | num_params_taken = yield args[i], params 546 | unless num_params_taken 547 | if @stop_on_unknown 548 | remains += args[i + 1 .. -1] 549 | return remains 550 | else 551 | remains += params 552 | end 553 | end 554 | i += 1 + num_params_taken 555 | else # long argument no parameter 556 | yield args[i], nil 557 | i += 1 558 | end 559 | when /^-(\S+)$/ # one or more short arguments 560 | shortargs = $1.split(//) 561 | shortargs.each_with_index do |a, j| 562 | if j == (shortargs.length - 1) 563 | params = collect_argument_parameters(args, i + 1) 564 | unless params.empty? 565 | num_params_taken = yield "-#{a}", params 566 | unless num_params_taken 567 | if @stop_on_unknown 568 | remains += args[i + 1 .. -1] 569 | return remains 570 | else 571 | remains += params 572 | end 573 | end 574 | i += 1 + num_params_taken 575 | else # argument no parameter 576 | yield "-#{a}", nil 577 | i += 1 578 | end 579 | else 580 | yield "-#{a}", nil 581 | end 582 | end 583 | else 584 | if @stop_on_unknown 585 | remains += args[i .. -1] 586 | return remains 587 | else 588 | remains << args[i] 589 | i += 1 590 | end 591 | end 592 | end 593 | 594 | remains 595 | end 596 | 597 | def parse_integer_parameter param, arg 598 | raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/ 599 | param.to_i 600 | end 601 | 602 | def parse_float_parameter param, arg 603 | raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE 604 | param.to_f 605 | end 606 | 607 | def parse_io_parameter param, arg 608 | case param 609 | when /^(stdin|-)$/i; $stdin 610 | else 611 | require 'open-uri' 612 | begin 613 | open param 614 | rescue SystemCallError => e 615 | raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}" 616 | end 617 | end 618 | end 619 | 620 | def collect_argument_parameters args, start_at 621 | params = [] 622 | pos = start_at 623 | while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do 624 | params << args[pos] 625 | pos += 1 626 | end 627 | params 628 | end 629 | 630 | def resolve_default_short_options 631 | @order.each do |type, name| 632 | next unless type == :opt 633 | opts = @specs[name] 634 | next if opts[:short] 635 | 636 | c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) } 637 | if c # found a character to use 638 | opts[:short] = c 639 | @short[c] = name 640 | end 641 | end 642 | end 643 | 644 | def wrap_line str, opts={} 645 | prefix = opts[:prefix] || 0 646 | width = opts[:width] || (self.width - 1) 647 | start = 0 648 | ret = [] 649 | until start > str.length 650 | nextt = 651 | if start + width >= str.length 652 | str.length 653 | else 654 | x = str.rindex(/\s/, start + width) 655 | x = str.index(/\s/, start) if x && x < start 656 | x || str.length 657 | end 658 | ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt] 659 | start = nextt + 1 660 | end 661 | ret 662 | end 663 | 664 | ## instance_eval but with ability to handle block arguments 665 | ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html 666 | def cloaker &b 667 | (class << self; self; end).class_eval do 668 | define_method :cloaker_, &b 669 | meth = instance_method :cloaker_ 670 | remove_method :cloaker_ 671 | meth 672 | end 673 | end 674 | end 675 | 676 | ## The easy, syntactic-sugary entry method into Trollop. Creates a Parser, 677 | ## passes the block to it, then parses +args+ with it, handling any errors or 678 | ## requests for help or version information appropriately (and then exiting). 679 | ## Modifies +args+ in place. Returns a hash of option values. 680 | ## 681 | ## The block passed in should contain zero or more calls to +opt+ 682 | ## (Parser#opt), zero or more calls to +text+ (Parser#text), and 683 | ## probably a call to +version+ (Parser#version). 684 | ## 685 | ## The returned block contains a value for every option specified with 686 | ## +opt+. The value will be the value given on the commandline, or the 687 | ## default value if the option was not specified on the commandline. For 688 | ## every option specified on the commandline, a key "