├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── docs └── development.md ├── examples ├── foobar_test.rb └── test_construct_spec.rb ├── lib ├── test_construct.rb └── test_construct │ ├── helpers.rb │ ├── pathname_extensions.rb │ ├── rspec_integration.rb │ └── version.rb ├── test ├── rspec_integration_test.rb ├── test_construct_test.rb └── test_helper.rb └── test_construct.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .ruby-version 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TestConstruct Changelog 2 | 3 | ## v2.0.2 4 | 5 | * Fixes line endings on Windows (@MSP-Greg) 6 | 7 | ## v2.0.1 8 | 9 | * Adds support for RSpec 3 (@jcouball) 10 | 11 | ## v2.0.0 12 | 13 | Enhancements: 14 | 15 | * Add RSpec 2 support (@avdi) 16 | * Option to persist directories after test run or after failure (@avdi) 17 | * Add naming of created directories (@avdi) 18 | 19 | Other: 20 | 21 | * Non-backwards-compatible change to `TestConstruct::Helpers#create_construct` method signature (takes option hash now) 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in test_construct.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2019 Ben Brinckerhoff 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/test_construct.svg)](https://badge.fury.io/rb/test_construct) 2 | 3 | # TestConstruct 4 | 5 | > "This is the construct. It's our loading program. We can load anything, from clothing to equipment, weapons, and training simulations, anything we need" -- Morpheus 6 | 7 | TestConstruct is a DSL for creating temporary files and directories during testing. 8 | 9 | ## SYNOPSIS 10 | 11 | ```ruby 12 | class ExampleTest < Test::Unit::TestCase 13 | include TestConstruct::Helpers 14 | 15 | def test_example 16 | within_construct do |c| 17 | c.directory 'alice/rabbithole' do |d| 18 | d.file 'white_rabbit.txt', "I'm late!" 19 | 20 | assert_equal "I'm late!", File.read('white_rabbit.txt') 21 | end 22 | end 23 | end 24 | 25 | end 26 | ``` 27 | 28 | ## Installation 29 | 30 | Add this line to your application's Gemfile: 31 | 32 | gem 'test_construct' 33 | 34 | And then execute: 35 | 36 | $ bundle 37 | 38 | Or install it yourself as: 39 | 40 | $ gem install test_construct 41 | 42 | ## Usage 43 | 44 | To use TestConstruct, you need to include the TestConstruct module in your class like so: 45 | 46 | include TestConstruct::Helpers 47 | 48 | Using construct is as simple as calling `within_construct` and providing a block. All files and directories that are created within that block are created within a temporary directory. The temporary directory is always deleted before `within_construct` finishes. 49 | 50 | There is nothing special about the files and directories created with TestConstruct, so you can use plain old Ruby IO methods to interact with them. 51 | 52 | ### Creating files 53 | 54 | The most basic use of TestConstruct is creating an empty file with the: 55 | 56 | ```ruby 57 | within_construct do |construct| 58 | construct.file('foo.txt') 59 | end 60 | ``` 61 | 62 | Note that the working directory is, by default, automatically changed to the temporary directory created by TestConstruct, so the following assertion will pass: 63 | 64 | ```ruby 65 | within_construct do |construct| 66 | construct.file('foo.txt') 67 | assert File.exist?('foo.txt') 68 | end 69 | ``` 70 | 71 | You can also provide content for the file, either with an optional argument or using the return value of a supplied block: 72 | 73 | ```ruby 74 | within_construct do |construct| 75 | construct.file('foo.txt', 'Here is some content') 76 | construct.file('bar.txt') do 77 | <<-EOS 78 | The block will return this string, which will be used as the content. 79 | EOS 80 | end 81 | end 82 | ``` 83 | 84 | If you provide block that accepts a parameter, construct will pass in the IO object. In this case, you are responsible for writing content to the file yourself - the return value of the block will not be used: 85 | 86 | ```ruby 87 | within_construct do |construct| 88 | construct.file('foo.txt') do |file| 89 | file << "Some content\n" 90 | file << "Some more content" 91 | end 92 | end 93 | ``` 94 | 95 | Finally, you can provide the entire path to a file and the parent directories will be created automatically: 96 | 97 | ```ruby 98 | within_construct do |construct| 99 | construct.file('foo/bar/baz.txt') 100 | end 101 | ``` 102 | 103 | ### Creating directories 104 | 105 | It is easy to create a directory: 106 | 107 | ```ruby 108 | within_construct do |construct| 109 | construct.directory('foo') 110 | end 111 | ``` 112 | 113 | You can also provide a block. The object passed to the block can be used to create nested files and directories (it's just a [Pathname](http://www.ruby-doc.org/stdlib/libdoc/pathname/rdoc/index.html) instance with some extra functionality, so you can use it to get the path of the current directory). 114 | 115 | Again, note that the working directory is automatically changed while in the block: 116 | 117 | ```ruby 118 | within_construct do |construct| 119 | construct.directory('foo') do |dir| 120 | dir.file('bar.txt') 121 | assert File.exist?('bar.txt') # This assertion will pass 122 | end 123 | end 124 | ``` 125 | 126 | Again, you can provide paths and the necessary directories will be automatically created: 127 | 128 | 129 | ```ruby 130 | within_construct do |construct| 131 | construct.directory('foo/bar/') do |dir| 132 | dir.directory('baz') 133 | dir.directory('bazz') 134 | end 135 | end 136 | ``` 137 | 138 | Please read [test/construct_test.rb](test/construct_test.rb) for more examples. 139 | 140 | ### Disabling chdir 141 | 142 | In some cases, you may wish to disable the default behavior of automatically changing the current directory. For example, changing the current directory will prevent Ruby debuggers from displaying source code correctly. 143 | 144 | If you disable, automatic chdir, note that your old assertions will not work: 145 | 146 | ```ruby 147 | within_construct(:chdir => false) do |construct| 148 | construct.file("foo.txt") 149 | # Fails. foo.txt was created in construct, but 150 | # the current directory is not the construct! 151 | assert File.exists?("foo.txt") 152 | end 153 | ``` 154 | 155 | To fix, simply use the `Pathname` passed to the block: 156 | 157 | ```ruby 158 | within_construct(:chdir => false) do |construct| 159 | construct.file("foo.txt") 160 | # Passes 161 | assert File.exists?(construct+"foo.txt") 162 | end 163 | ``` 164 | 165 | ### Keeping directories around 166 | 167 | You may find it convenient to keep the created directory around after a test has completed, in order to manually inspect its contents. 168 | 169 | ```ruby 170 | within_construct do |construct| 171 | # ... 172 | construct.keep 173 | end 174 | ``` 175 | 176 | Most likely you only want the directory to stick around if something goes wrong. To do this, use the `:keep_on_error` option. 177 | 178 | ```ruby 179 | within_construct(keep_on_error: true) do |construct| 180 | # ... 181 | raise "some error" 182 | end 183 | ``` 184 | 185 | TestConstruct will also annotate the exception error message to tell you where the generated files can be found. 186 | 187 | ### Setting the base directory 188 | 189 | By default, TestConstruct puts its temporary container directories in your system temp dir. You can change this with the `:base_dir` option: 190 | 191 | ```ruby 192 | tmp_dir = File.expand_path("../../tmp", __FILE__) 193 | within_construct(base_dir: tmp_dir) do |construct| 194 | construct.file("foo.txt") 195 | # Passes 196 | assert File.exists?(tmp_dir+"/foo.txt") 197 | end 198 | ``` 199 | 200 | ### Naming the created directories 201 | 202 | Normally TestConstruct names the container directories it creates using a combination of a `test-construct-` prefix, the current process ID, and a random number. This ensures that the name is unlikely to clash with directories left over from previous runs. However, it isn't very meaningful. You can optionally make the directory names more recognizable by specifying a `:name` option. TestConstruct will take the string passed, turn it into a normalized "slug" without any funny characters, and append it to the end of the generated dirname. 203 | 204 | ```ruby 205 | within_construct(name: "My best test ever!") do |construct| 206 | # will generate something like: 207 | # /tmp/construct-container-1234-5678-my-best-test-ever 208 | end 209 | ``` 210 | 211 | ### RSpec Integration 212 | 213 | TestConstruct comes with RSpec integration. Just require the `test_construct/rspec_integration` file in your `spec_helper.rb` or in your spec file. Then tag the tests you want to execute in the context of a construct container with `test_construct: true` using RSpec metadata: 214 | 215 | ```ruby 216 | require "test_construct/rspec_integration" 217 | 218 | describe Foo, test_construct: true do 219 | it "should do stuff" do 220 | example.metadata[:construct].file "somefile" 221 | example.metadata[:construct].directory "somedir" 222 | # ... 223 | end 224 | end 225 | ``` 226 | 227 | By default, the current working directory will be switched to the construct container within tests; the container name will be derived from the name of the current example; and if a test fails, the container will be kept around. Information about where to find it will be added to the test failure message. 228 | 229 | You can tweak any TestConstruct options by passing a hash as the value of the `:test_construct` metadata key. 230 | 231 | ```ruby 232 | require "test_construct/rspec_integration" 233 | 234 | describe Foo, test_construct: {keep_on_error: false} do 235 | # ... 236 | end 237 | ``` 238 | 239 | ## Contributing 240 | 241 | 1. Fork it 242 | 2. Create your feature branch (`git checkout -b my-new-feature`) 243 | 3. Commit your changes (`git commit -am 'Add some feature'`) 244 | 4. Push to the branch (`git push origin my-new-feature`) 245 | 5. Create new Pull Request 246 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | 10 | task :default => [:test] 11 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Release 4 | 5 | 1. `rake test` 6 | 1. Update version in `lib/test_construct/version.rb` 7 | 1. Update `CHANGELOG` 8 | 1. `rake build` 9 | 2. `rake release` -------------------------------------------------------------------------------- /examples/foobar_test.rb: -------------------------------------------------------------------------------- 1 | # Run with: 2 | # ruby -Ilib examples/foobar_test.rb 3 | 4 | require 'test_construct' 5 | require 'test/unit' 6 | 7 | class FoobarTest < Test::Unit::TestCase 8 | include TestConstruct::Helpers 9 | 10 | def test_directory_and_files 11 | within_construct do |c| 12 | c.directory 'alice/rabbithole' do |d| 13 | d.file 'white_rabbit.txt', "I'm late!" 14 | 15 | assert_equal "I'm late!", File.read('white_rabbit.txt') 16 | end 17 | end 18 | end 19 | 20 | def test_keeping_directory_on_error 21 | within_construct(keep_on_error: true) do |c| 22 | c.directory 'd' do |d| 23 | d.file 'doughnut.txt' 24 | raise "whoops" 25 | end 26 | end 27 | end 28 | 29 | def test_deleting_directory_on_error 30 | within_construct(keep_on_error: false) do |c| 31 | c.directory 'd' do |d| 32 | d.file 'doughnut.txt' 33 | raise "whoops" 34 | end 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /examples/test_construct_spec.rb: -------------------------------------------------------------------------------- 1 | # Run with: 2 | # rspec --format doc --order defined examples/test_construct_spec.rb 3 | 4 | # We can't replace the usage of the instance variable with a let or method call, 5 | # since the example isn't available then, so we disable the warning: 6 | # rubocop:disable RSpec/InstanceVariable 7 | 8 | require "test_construct/rspec_integration" 9 | 10 | RSpec.describe TestConstruct do 11 | before do 12 | stub_const("TEST_PATH", "alice/rabbithole") 13 | stub_const("TEST_FILE", "white_rabbit.txt") 14 | stub_const("TEST_CONTENTS", "I'm late!") 15 | end 16 | 17 | describe "when not enabled in describe block" do 18 | it "is disabled in examples" do |example| 19 | expect(example.metadata).not_to include(:construct) 20 | end 21 | 22 | it "can be enabled for example", test_construct: true do |example| 23 | expect(example.metadata).to include(:construct) 24 | end 25 | 26 | context "when enabled in context", test_construct: true do 27 | it "is enabled in examples" do |example| 28 | expect(example.metadata).to include(:construct) 29 | end 30 | end 31 | end 32 | 33 | describe "when enabled in describe block", test_construct: true do 34 | before { |example| @construct = example.metadata[:construct] } 35 | 36 | it "can be disabled for an example", test_construct: false do 37 | expect(@context).to be_nil 38 | end 39 | 40 | context "when disabled in context", test_construct: false do 41 | it "is disabled in examples" do |example| 42 | expect(example.metadata).not_to include(:construct) 43 | end 44 | end 45 | 46 | it "leaves file on error" do 47 | @construct.directory TEST_PATH do |path| 48 | path.file(TEST_FILE, TEST_CONTENTS) 49 | raise "Expected to fail" 50 | end 51 | end 52 | 53 | context "when keep_on_error is disabled for an example" do 54 | it "doesn't leave file", test_construct: { keep_on_error: false } do 55 | @construct.directory TEST_PATH do |path| 56 | path.file TEST_FILE, TEST_CONTENTS 57 | raise "Expected to fail" 58 | end 59 | end 60 | end 61 | 62 | describe ".directory" do 63 | it "creates directory" do 64 | @construct.directory("alice") 65 | expect(Dir.exist?("alice")).to be true 66 | end 67 | 68 | it "creates directory and sudirectories" do 69 | @construct.directory(TEST_PATH) 70 | expect(Dir.exist?(TEST_PATH)).to be true 71 | end 72 | 73 | it "runs block in directory" do 74 | @construct.directory TEST_PATH do |path| 75 | path.file TEST_FILE, TEST_CONTENTS 76 | end 77 | filename = File.join(TEST_PATH, TEST_FILE) 78 | expect(File.read(filename)).to eq(TEST_CONTENTS) 79 | end 80 | end 81 | 82 | describe ".file" do 83 | it "creates empty file" do 84 | @construct.file(TEST_FILE) 85 | expect(File.read(TEST_FILE)).to eq("") 86 | end 87 | 88 | it "creates file with content from optional argument" do 89 | @construct.file(TEST_FILE, TEST_CONTENTS) 90 | expect(File.read(TEST_FILE)).to eq(TEST_CONTENTS) 91 | end 92 | 93 | it "creates file with content from block" do 94 | @construct.file(TEST_FILE) { TEST_CONTENTS } 95 | expect(File.read(TEST_FILE)).to eq(TEST_CONTENTS) 96 | end 97 | 98 | it "creates file and provides block IO object" do 99 | @construct.file(TEST_FILE) { |file| file << TEST_CONTENTS } 100 | expect(File.read(TEST_FILE)).to eq(TEST_CONTENTS) 101 | end 102 | end 103 | end 104 | 105 | describe "when keep_on_error is disabled in describe block", 106 | test_construct: { keep_on_error: false } do 107 | it "doesn't leave file on error" do |example| 108 | example.metadata[:construct].directory TEST_PATH do |path| 109 | path.file TEST_FILE, TEST_CONTENTS 110 | raise "Expected to fail" 111 | end 112 | end 113 | end 114 | end 115 | 116 | # rubocop:enable RSpec/InstanceVariable 117 | -------------------------------------------------------------------------------- /lib/test_construct.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | 3 | require "test_construct/version" 4 | require "test_construct/helpers" 5 | require "test_construct/pathname_extensions" 6 | 7 | module TestConstruct 8 | 9 | CONTAINER_PREFIX = 'construct_container' 10 | 11 | def self.tmpdir 12 | dir = nil 13 | Dir.chdir Dir.tmpdir do dir = Dir.pwd end # HACK FOR OS X 14 | dir 15 | end 16 | 17 | def self.destroy_all! 18 | Pathname.glob(File.join(tmpdir, CONTAINER_PREFIX + "*")) do |container| 19 | container.rmtree 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/test_construct/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'pathname' 3 | 4 | module TestConstruct 5 | 6 | module Helpers 7 | extend self 8 | 9 | def within_construct(opts = {}) 10 | container = setup_construct(opts) 11 | yield(container) 12 | rescue Exception => error 13 | raise unless container 14 | teardown_construct(container, error, opts) 15 | raise error 16 | else 17 | teardown_construct(container, nil, opts) 18 | end 19 | 20 | def create_construct(opts = {}) 21 | chdir_default = opts.delete(:chdir) { true } 22 | base_path = Pathname(opts.delete(:base_dir) { TestConstruct.tmpdir }) 23 | name = opts.delete(:name) { "" } 24 | slug = name.downcase.tr_s("^a-z0-9", "-")[0..63] 25 | if opts.any? 26 | raise "[TestConstruct] Unrecognized options: #{opts.keys}" 27 | end 28 | dir = "#{CONTAINER_PREFIX}-#{$PROCESS_ID}-#{rand(1_000_000_000)}" 29 | dir << "-" << slug unless slug.empty? 30 | path = base_path + dir 31 | path.mkpath 32 | path.extend(PathnameExtensions) 33 | path.construct__chdir_default = chdir_default 34 | path 35 | end 36 | 37 | # THIS METHOD MAY HAVE EXTERNAL SIDE-EFFECTS, including: 38 | # - creating the container directory tree 39 | # - changing the current working directory 40 | # 41 | # It is intended to be paired with #teardown_construct 42 | def setup_construct(opts = {}) 43 | opts = opts.dup 44 | chdir = opts.fetch(:chdir, true) 45 | opts.delete(:keep_on_error) { false } # not used in setup 46 | container = create_construct(opts) 47 | container.maybe_change_dir(chdir) 48 | container 49 | end 50 | 51 | # THIS METHOD MAY HAVE EXTERNAL SIDE-EFFECTS, including: 52 | # - removing the container directory tree 53 | # - changing the current working directory 54 | # - modifying any exception passed as `error` 55 | # 56 | # It is intended to be paired with #setup_construct 57 | def teardown_construct(container, error = nil, opts = {}) 58 | if error && opts[:keep_on_error] 59 | container.keep 60 | container.annotate_exception!(error) 61 | end 62 | container.finalize 63 | end 64 | end 65 | 66 | extend Helpers 67 | end 68 | -------------------------------------------------------------------------------- /lib/test_construct/pathname_extensions.rb: -------------------------------------------------------------------------------- 1 | module TestConstruct 2 | module PathnameExtensions 3 | 4 | attr_accessor :construct__chdir_default, :construct__root, :construct__orig_dir 5 | def directory(path, opts = {}) 6 | chdir = opts.fetch(:chdir, construct__chdir_default) 7 | subdir = (self + path) 8 | subdir.mkpath 9 | subdir.extend(PathnameExtensions) 10 | subdir.construct__root = construct__root || self 11 | subdir.maybe_change_dir(chdir) do 12 | yield(subdir) if block_given? 13 | end 14 | subdir 15 | end 16 | 17 | def file(filepath, contents = nil, &block) 18 | path = (self+filepath) 19 | path.dirname.mkpath 20 | mode = RUBY_PLATFORM =~ /mingw|mswin/ ? 'wb:UTF-8' : 'w' 21 | File.open(path, mode) do |f| 22 | if(block) 23 | if(block.arity==1) 24 | block.call(f) 25 | else 26 | f << block.call 27 | end 28 | else 29 | f << contents 30 | end 31 | end 32 | path 33 | end 34 | 35 | def maybe_change_dir(chdir, &block) 36 | if(chdir) 37 | self.construct__orig_dir ||= Pathname.pwd 38 | self.chdir(&block) 39 | else 40 | block.call if block 41 | end 42 | end 43 | 44 | def revert_cwd 45 | if construct__orig_dir 46 | Dir.chdir(construct__orig_dir) 47 | end 48 | end 49 | 50 | # Note: Pathname implements #chdir directly, but it is deprecated in favor 51 | # of Dir.chdir 52 | def chdir(&block) 53 | Dir.chdir(self, &block) 54 | end 55 | 56 | def destroy! 57 | rmtree 58 | end 59 | 60 | def finalize 61 | revert_cwd 62 | destroy! unless keep? 63 | end 64 | 65 | def keep 66 | if construct__root 67 | construct__root.keep 68 | else 69 | @keep = true 70 | end 71 | end 72 | 73 | def keep? 74 | defined?(@keep) && @keep 75 | end 76 | 77 | def annotate_exception!(error) 78 | error.message << exception_message_annotation 79 | error 80 | end 81 | 82 | def exception_message_annotation 83 | "\nTestConstruct files kept at: #{self}" 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/test_construct/rspec_integration.rb: -------------------------------------------------------------------------------- 1 | require "test_construct" 2 | require "rspec" 3 | module TestConstruct 4 | module RSpecIntegration 5 | module_function 6 | 7 | # the :test_construct metadata key can be either: 8 | # - true (for all defaults) 9 | # - a Hash of options 10 | # - false/missing (disable the construct for this test) 11 | def test_construct_options(example) 12 | options = test_construct_default_options 13 | options[:name] = example.full_description 14 | metadata_options = example.metadata[:test_construct] 15 | if metadata_options.is_a?(Hash) 16 | options.merge!(metadata_options) 17 | end 18 | options 19 | end 20 | 21 | def test_construct_enabled?(example) 22 | !!example.metadata[:test_construct] 23 | end 24 | 25 | def test_construct_default_options 26 | { 27 | base_dir: TestConstruct.tmpdir, 28 | chdir: true, 29 | keep_on_error: true, 30 | } 31 | end 32 | end 33 | end 34 | 35 | RSpec.configure do |config| 36 | config.include TestConstruct::Helpers 37 | config.include TestConstruct::RSpecIntegration 38 | 39 | version = RSpec::Version::STRING 40 | major, minor, patch, rest = version.split(".") 41 | major, minor, patch = [major, minor, patch].map(&:to_i) 42 | 43 | def before_each(example) 44 | return unless test_construct_enabled?(example) 45 | options = test_construct_options(example) 46 | example.metadata[:construct] = setup_construct(options) 47 | end 48 | 49 | def after_each(example) 50 | return unless test_construct_enabled?(example) 51 | options = test_construct_options(example) 52 | teardown_construct( 53 | example.metadata[:construct], 54 | example.exception, 55 | options) 56 | end 57 | 58 | if major == 2 && minor >= 7 59 | config.before :each do 60 | before_each(example) 61 | end 62 | 63 | config.after :each do 64 | after_each(example) 65 | end 66 | elsif major == 3 67 | config.before :each do |example| 68 | before_each(example) 69 | end 70 | 71 | config.after :each do |example| 72 | after_each(example) 73 | end 74 | else 75 | raise "TestConstruct does not support RSpec #{version}" 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/test_construct/version.rb: -------------------------------------------------------------------------------- 1 | module TestConstruct 2 | VERSION = "2.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /test/rspec_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RspecIntegrationTest < Minitest::Test 4 | include TestConstruct::Helpers 5 | 6 | test 'rspec integration' do 7 | lib_path = File.realpath('lib') 8 | within_construct do |construct| 9 | spec_file_name = 'rspec_spec.rb' 10 | construct.file(spec_file_name, <<-RSPEC) 11 | require 'test_construct/rspec_integration' 12 | 13 | describe 'test_construct', test_construct: true do 14 | it 'accesses metadata' do |example| 15 | f = example.metadata[:construct].file "somefile", "abcd" 16 | expect(f.size).to eq 4 17 | end 18 | end 19 | RSPEC 20 | output = `rspec -I '#{lib_path}' #{spec_file_name}` 21 | assert $CHILD_STATUS.success?, output 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_construct_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestConstructTest < Minitest::Test 4 | include TestConstruct::Helpers 5 | 6 | def teardown 7 | Dir.chdir File.expand_path("../..", __FILE__) 8 | TestConstruct.destroy_all! 9 | end 10 | 11 | testing 'using within_construct explicitly' do 12 | 13 | test 'creates construct' do 14 | num = rand(1_000_000_000) 15 | TestConstruct.stubs(:rand).returns(num) 16 | directory = false 17 | TestConstruct::within_construct do |construct| 18 | directory = File.directory?(File.join(TestConstruct.tmpdir, "construct_container-#{$PROCESS_ID}-#{num}")) 19 | end 20 | assert directory 21 | 22 | directory = false 23 | TestConstruct.within_construct do |construct| 24 | directory = File.directory?(File.join(TestConstruct.tmpdir, "construct_container-#{$PROCESS_ID}-#{num}")) 25 | end 26 | assert directory 27 | end 28 | 29 | end 30 | 31 | testing 'creating a construct container' do 32 | 33 | test 'exists' do 34 | num = rand(1_000_000_000) 35 | self.stubs(:rand).returns(num) 36 | within_construct do |construct| 37 | assert File.directory?(File.join(TestConstruct.tmpdir, "construct_container-#{$PROCESS_ID}-#{num}")) 38 | end 39 | end 40 | 41 | test 'yields to its block' do 42 | sensor = 'no yield' 43 | within_construct do 44 | sensor = 'yielded' 45 | end 46 | assert_equal 'yielded', sensor 47 | end 48 | 49 | test 'block argument is container directory Pathname' do 50 | num = rand(1_000_000_000) 51 | self.stubs(:rand).returns(num) 52 | within_construct do |container_path| 53 | expected_path = (Pathname(TestConstruct.tmpdir) + 54 | "construct_container-#{$PROCESS_ID}-#{num}") 55 | assert_equal(expected_path, container_path) 56 | end 57 | end 58 | 59 | test 'does not exist afterwards' do 60 | path = nil 61 | within_construct do |container_path| 62 | path = container_path 63 | end 64 | assert !path.exist? 65 | end 66 | 67 | test 'removes entire tree afterwards' do 68 | path = nil 69 | within_construct do |container_path| 70 | path = container_path 71 | (container_path + 'foo').mkdir 72 | end 73 | assert !path.exist? 74 | end 75 | 76 | test 'removes dir if block raises exception' do 77 | path = nil 78 | begin 79 | within_construct do |container_path| 80 | path = container_path 81 | raise 'something bad happens here' 82 | end 83 | rescue 84 | end 85 | assert !path.exist? 86 | end 87 | 88 | test 'does not capture exceptions raised in block' do 89 | err = RuntimeError.new('an error') 90 | begin 91 | within_construct do 92 | raise err 93 | end 94 | rescue RuntimeError => e 95 | assert_same err, e 96 | end 97 | end 98 | 99 | end 100 | 101 | testing 'creating a file in a container' do 102 | 103 | test 'exists while in construct block' do 104 | within_construct do |construct| 105 | construct.file('foo.txt') 106 | assert File.exist?(construct + 'foo.txt') 107 | end 108 | end 109 | 110 | test 'does not exist after construct block' do 111 | filepath = 'unset' 112 | within_construct do |construct| 113 | filepath = construct.file('foo.txt') 114 | end 115 | assert !File.exist?(filepath) 116 | end 117 | 118 | test 'has empty file contents by default' do 119 | within_construct do |construct| 120 | construct.file('foo.txt') 121 | assert_equal '', File.read(construct + 'foo.txt') 122 | end 123 | end 124 | 125 | test 'writes contents to file' do 126 | within_construct do |construct| 127 | construct.file('foo.txt','abcxyz') 128 | assert_equal 'abcxyz', File.read(construct+'foo.txt') 129 | end 130 | end 131 | 132 | test 'contents can be given in a block' do 133 | within_construct do |construct| 134 | construct.file('foo.txt') do 135 | <<-EOS 136 | File 137 | Contents 138 | EOS 139 | end 140 | assert_equal "File\nContents\n", File.read(construct+'foo.txt') 141 | end 142 | end 143 | 144 | test 'contents block overwrites contents argument' do 145 | within_construct do |construct| 146 | construct.file('foo.txt','abc') do 147 | 'xyz' 148 | end 149 | assert_equal 'xyz', File.read(construct+'foo.txt') 150 | end 151 | end 152 | 153 | test 'block is passed File object' do 154 | within_construct do |construct| 155 | construct.file('foo.txt') do |file| 156 | assert_equal((construct+'foo.txt').to_s, file.path) 157 | end 158 | end 159 | end 160 | 161 | test 'writes to File object passed to block' do 162 | within_construct do |construct| 163 | construct.file('foo.txt') do |file| 164 | file << 'abc' 165 | end 166 | assert_equal 'abc', File.read(construct+'foo.txt') 167 | end 168 | end 169 | 170 | test 'closes file after block ends' do 171 | within_construct do |construct| 172 | construct_file = nil 173 | construct.file('foo.txt') do |file| 174 | construct_file = file 175 | end 176 | assert construct_file.closed? 177 | end 178 | end 179 | 180 | test 'block return value not used as content if passed File object' do 181 | within_construct do |construct| 182 | construct.file('foo.txt') do |file| 183 | file << 'abc' 184 | 'xyz' 185 | end 186 | assert_equal 'abc', File.read(construct+'foo.txt') 187 | end 188 | end 189 | 190 | test 'contents argument is ignored if block takes File arg' do 191 | within_construct do |construct| 192 | construct.file('foo.txt','xyz') do |file| 193 | file << 'abc' 194 | end 195 | assert_equal 'abc', File.read(construct+'foo.txt') 196 | end 197 | end 198 | 199 | test 'returns file path' do 200 | within_construct do |construct| 201 | assert_equal(construct+'foo.txt', construct.file('foo.txt')) 202 | end 203 | end 204 | 205 | test 'creates file including path in one call' do 206 | within_construct do |construct| 207 | construct.file('foo/bar/baz.txt') 208 | assert (construct+'foo/bar/baz.txt').exist? 209 | end 210 | end 211 | 212 | test 'creates file including path in one call when directories exists' do 213 | within_construct do |construct| 214 | construct.directory('foo/bar') 215 | construct.file('foo/bar/baz.txt') 216 | assert (construct+'foo/bar/baz.txt').exist? 217 | end 218 | end 219 | 220 | test 'creates file including path with chained calls' do 221 | within_construct do |construct| 222 | construct.directory('foo').directory('bar').file('baz.txt') 223 | assert (construct+'foo/bar/baz.txt').exist? 224 | end 225 | end 226 | 227 | end 228 | 229 | testing 'creating a subdirectory in container' do 230 | 231 | test 'exists while in construct block' do 232 | within_construct do |construct| 233 | construct.directory 'foo' 234 | assert (construct+'foo').directory? 235 | end 236 | end 237 | 238 | test 'does not exist after construct block' do 239 | subdir = 'unset' 240 | within_construct do |construct| 241 | construct.directory 'foo' 242 | subdir = construct + 'foo' 243 | end 244 | assert !subdir.directory? 245 | end 246 | 247 | test 'returns the new path name' do 248 | within_construct do |construct| 249 | assert_equal((construct+'foo'), construct.directory('foo')) 250 | end 251 | end 252 | 253 | test 'yield to block' do 254 | sensor = 'unset' 255 | within_construct do |construct| 256 | construct.directory('bar') do 257 | sensor = 'yielded' 258 | end 259 | end 260 | assert_equal 'yielded', sensor 261 | end 262 | 263 | test 'block argument is subdirectory path' do 264 | within_construct do |construct| 265 | construct.directory('baz') do |dir| 266 | assert_equal((construct+'baz'),dir) 267 | end 268 | end 269 | end 270 | 271 | test 'creates nested directory in one call' do 272 | within_construct do |construct| 273 | construct.directory('foo/bar') 274 | assert (construct+'foo/bar').directory? 275 | end 276 | end 277 | 278 | test 'creates a nested directory in two calls' do 279 | within_construct do |construct| 280 | construct.directory('foo').directory('bar') 281 | assert (construct+'foo/bar').directory? 282 | end 283 | end 284 | 285 | end 286 | 287 | testing "subdirectories changing the working directory" do 288 | 289 | test 'forces directory stays the same' do 290 | within_construct do |construct| 291 | old_pwd = Dir.pwd 292 | construct.directory('foo', :chdir => false) do 293 | assert_equal old_pwd, Dir.pwd 294 | end 295 | end 296 | end 297 | 298 | test 'defaults chdir setting from construct' do 299 | within_construct(:chdir => false) do |construct| 300 | old_pwd = Dir.pwd 301 | construct.directory('foo') do 302 | assert_equal old_pwd, Dir.pwd 303 | end 304 | end 305 | end 306 | 307 | test 'overrides construct default' do 308 | within_construct(:chdir => false) do |construct| 309 | construct.directory('foo', :chdir => true) do |dir| 310 | assert_equal dir.to_s, Dir.pwd 311 | end 312 | end 313 | end 314 | 315 | test 'current working directory is within subdirectory' do 316 | within_construct do |construct| 317 | construct.directory('foo') do |dir| 318 | assert_equal dir.to_s, Dir.pwd 319 | end 320 | end 321 | end 322 | 323 | test 'current working directory is unchanged outside of subdirectory' do 324 | within_construct do |construct| 325 | old_pwd = Dir.pwd 326 | construct.directory('foo') 327 | assert_equal old_pwd, Dir.pwd 328 | end 329 | end 330 | 331 | test 'current working directory is unchanged after exception' do 332 | within_construct do |construct| 333 | old_pwd = Dir.pwd 334 | begin 335 | construct.directory('foo') do 336 | raise 'something bad happens here' 337 | end 338 | rescue 339 | end 340 | assert_equal old_pwd, Dir.pwd 341 | end 342 | end 343 | 344 | test 'captures exceptions raised in block' do 345 | within_construct do |construct| 346 | error = assert_raises RuntimeError do 347 | construct.directory('foo') do 348 | raise 'fail!' 349 | end 350 | end 351 | assert_equal 'fail!', error.message 352 | end 353 | end 354 | 355 | test 'checking for a file is relative to subdirectory' do 356 | within_construct do |construct| 357 | construct.directory('bar') do |dir| 358 | dir.file('foo.txt') 359 | assert File.exist?('foo.txt') 360 | end 361 | end 362 | end 363 | 364 | test 'checking for a directory is relative to subdirectory' do 365 | within_construct do |construct| 366 | construct.directory('foo') do |dir| 367 | dir.directory('mydir') 368 | assert File.directory?('mydir') 369 | end 370 | end 371 | end 372 | 373 | end 374 | 375 | testing "changing the working directory" do 376 | 377 | test 'forces directory stays the same' do 378 | old_pwd = Dir.pwd 379 | within_construct(:chdir => false) do |construct| 380 | assert_equal old_pwd, Dir.pwd 381 | end 382 | end 383 | 384 | test 'current working directory is within construct' do 385 | within_construct do |construct| 386 | assert_equal construct.to_s, Dir.pwd 387 | end 388 | end 389 | 390 | test 'current working directory is unchanged outside of construct' do 391 | old_pwd = Dir.pwd 392 | within_construct do |construct| 393 | end 394 | assert_equal old_pwd, Dir.pwd 395 | end 396 | 397 | test 'current working directory is unchanged after exception' do 398 | old_pwd = Dir.pwd 399 | begin 400 | within_construct do |construct| 401 | raise 'something bad happens here' 402 | end 403 | rescue 404 | end 405 | assert_equal old_pwd, Dir.pwd 406 | end 407 | 408 | test 'does not capture exceptions raised in block' do 409 | error = assert_raises RuntimeError do 410 | within_construct do 411 | raise 'fail!' 412 | end 413 | end 414 | assert_equal 'fail!', error.message 415 | end 416 | 417 | test 'checking for a file is relative to container' do 418 | within_construct do |construct| 419 | construct.file('foo.txt') 420 | assert File.exist?('foo.txt') 421 | end 422 | end 423 | 424 | test 'checking for a directory is relative to container' do 425 | within_construct do |construct| 426 | construct.directory('mydir') 427 | assert File.directory?('mydir') 428 | end 429 | end 430 | 431 | end 432 | 433 | testing "#create_construct" do 434 | 435 | test "returns a working Construct" do 436 | it = create_construct 437 | it.directory "foo" 438 | it.file "bar", "CONTENTS" 439 | assert (it + "foo").directory? 440 | assert_equal "CONTENTS", (it + "bar").read 441 | end 442 | 443 | end 444 | 445 | testing "#chdir" do 446 | 447 | test "executes its block in the context of the construct" do 448 | it = create_construct 449 | refute_equal it.to_s, Dir.pwd 450 | sensor = :unset 451 | it.chdir do 452 | sensor = Dir.pwd 453 | end 454 | assert_equal it.to_s, sensor 455 | end 456 | 457 | test "leaves construct directory on block exit" do 458 | it = create_construct 459 | it.chdir do 460 | # NOOP 461 | end 462 | refute_equal it.to_s, Dir.pwd 463 | end 464 | end 465 | 466 | testing "#destroy!" do 467 | test "removes the construct container" do 468 | it = create_construct 469 | it.destroy! 470 | assert !File.exist?(it.to_s) 471 | end 472 | end 473 | 474 | testing "#finalize" do 475 | test "removes the construct container" do 476 | it = create_construct 477 | it.finalize 478 | assert !File.exist?(it.to_s) 479 | end 480 | 481 | test "leaves the container if keep is flagged" do 482 | it = create_construct 483 | it.keep 484 | it.finalize 485 | assert File.exist?(it.to_s) 486 | end 487 | 488 | test "leaves the container if keep is flagged in a subdir" do 489 | it = create_construct 490 | subdir = it.directory "subdir" 491 | subdir.keep 492 | it.finalize 493 | assert File.exist?(it.to_s) 494 | end 495 | end 496 | 497 | testing "keep_on_error = true" do 498 | test 'keeps dir when block raises exception' do 499 | path = nil 500 | begin 501 | within_construct(keep_on_error: true) do |container_path| 502 | path = container_path 503 | raise 'something bad happens here' 504 | end 505 | rescue 506 | end 507 | assert path.exist? 508 | end 509 | 510 | test 'updates exception message to include location of files' do 511 | path = nil 512 | begin 513 | within_construct(keep_on_error: true) do |container_path| 514 | path = container_path 515 | raise 'bad stuff' 516 | end 517 | rescue => e 518 | error = e 519 | end 520 | assert_equal "bad stuff\nTestConstruct files kept at: #{path}", error.message 521 | end 522 | end 523 | 524 | testing 'base_dir option' do 525 | test 'determines the location of construct dirs' do 526 | base_dir = File.expand_path("../temp", __FILE__) 527 | within_construct(base_dir: base_dir) do |container| 528 | assert_equal base_dir, container.dirname.to_s 529 | end 530 | end 531 | end 532 | 533 | testing 'name option' do 534 | test 'used in generation of the directory name' do 535 | within_construct(name: "My best test ever!") do |container| 536 | assert_match(/my-best-test-ever-$/, container.basename.to_s) 537 | end 538 | end 539 | end 540 | end 541 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest' 2 | require "minitest/autorun" 3 | 4 | require 'mocha/minitest' 5 | require 'test_construct' 6 | 7 | class Minitest::Test 8 | 9 | def self.testing(name) 10 | @group = name 11 | yield 12 | @group = nil 13 | end 14 | 15 | def self.test(name, &block) 16 | name = name.strip.gsub(/\s\s+/, " ") 17 | group = "#{@group}: " if defined? @group 18 | test_name = "test_: #{group}#{name}".to_sym 19 | defined = instance_methods.include? test_name 20 | raise "#{test_name} is already defined in #{self}" if defined 21 | define_method(test_name, &block) 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /test_construct.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'test_construct/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "test_construct" 8 | spec.version = TestConstruct::VERSION 9 | spec.authors = ["Ben Brinckerhoff", "Avdi Grimm"] 10 | spec.email = ["ben@bbrinck.com", "avdi@avdi.org"] 11 | spec.description = %q{Creates temporary files and directories for testing.} 12 | spec.summary = %q{Creates temporary files and directories for testing.} 13 | spec.homepage = "https://github.com/bhb/test_construct" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", ">= 2.2.10" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "minitest", "~> 5.0.8" 24 | spec.add_development_dependency "mocha", "~> 0.14.0" 25 | spec.add_development_dependency "rspec", "~> 3.0" 26 | end 27 | --------------------------------------------------------------------------------