diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 217f60d..912003e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ['3.2', '3.1', '3.0', '2.7', '2.6'] + ruby-version: ['3.4', '3.3', '3.2', '3.1', '3.0', '2.7'] steps: - uses: actions/checkout@v3 @@ -25,10 +25,8 @@ jobs: with: ruby-version: ${{matrix.ruby-version}} bundler-cache: true - - name: Updating RubyGems - run: gem update --system - name: Install dependencies - run: bundle install + run: bundle install --quiet - name: Rubocop run: bundle exec rubocop -D - name: Rspec diff --git a/lib/memfs/dir.rb b/lib/memfs/dir.rb index 486d813..6707bc1 100644 --- a/lib/memfs/dir.rb +++ b/lib/memfs/dir.rb @@ -59,7 +59,10 @@ def self.getwd end class << self; alias pwd getwd; end - def self.glob(patterns, flags = 0, &block) + def self.glob(patterns, flags = 0, base: nil, sort: true, **opts, &block) + # Handle keyword argument for flags + flags = opts[:flags] if opts.key?(:flags) + patterns = [*patterns].map(&:to_s) list = fs.paths.select do |path| patterns.any? do |pattern| @@ -68,6 +71,7 @@ def self.glob(patterns, flags = 0, &block) end # FIXME: ugly special case for /* and / list.delete('/') if patterns.first == '/*' + return list unless block_given? list.each { |path| block.call(path) } nil @@ -101,6 +105,58 @@ def self.tmpdir '/tmp' end + def self.mktmpdir(prefix_suffix = nil, tmpdir = nil, **options) + tmpdir ||= self.tmpdir + + case prefix_suffix + when nil + prefix = 'd' + suffix = '' + when String + prefix = prefix_suffix + suffix = '' + when Array + prefix = prefix_suffix[0] || 'd' + suffix = prefix_suffix[1] || '' + else + raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" + end + + max_try = options.fetch(:max_try, 10000) + path = nil + + max_try.times do + timestamp = Time.now.strftime('%Y%m%d') + random = sprintf('%d-%d', $$, rand(0x100000000)) + path = File.join(tmpdir, "#{prefix}#{timestamp}-#{random}-0#{suffix}") + + begin + mkdir(path, 0o700) + break + rescue Errno::EEXIST + path = nil + next + end + end + + raise "cannot generate temporary directory name" if path.nil? + + if block_given? + begin + result = yield path + ensure + begin + rmdir(path) if exists?(path) + rescue + # Ignore cleanup errors + end + end + result + else + path + end + end + class << self alias delete rmdir alias unlink rmdir diff --git a/spec/memfs/dir_spec.rb b/spec/memfs/dir_spec.rb index cfd1cbe..6d3d1d4 100644 --- a/spec/memfs/dir_spec.rb +++ b/spec/memfs/dir_spec.rb @@ -423,6 +423,76 @@ module MemFs end end + describe '.mktmpdir' do + context 'when no block is given' do + it 'creates a temporary directory and returns its path' do + path = described_class.mktmpdir + expect(path).to start_with('/tmp/d') + expect(described_class.exist?(path)).to be true + end + + it 'accepts a prefix' do + path = described_class.mktmpdir('myprefix') + expect(path).to start_with('/tmp/myprefix') + expect(described_class.exist?(path)).to be true + end + + it 'accepts a prefix and suffix as an array' do + path = described_class.mktmpdir(['prefix_', '_suffix']) + expect(path).to start_with('/tmp/prefix_') + expect(path).to end_with('_suffix') + expect(described_class.exist?(path)).to be true + end + + it 'accepts a custom tmpdir' do + described_class.mkdir('/custom_tmp') + path = described_class.mktmpdir(nil, '/custom_tmp') + expect(path).to start_with('/custom_tmp/d') + expect(described_class.exist?(path)).to be true + end + end + + context 'when a block is given' do + it 'creates a temporary directory, yields it, and removes it' do + yielded_path = nil + described_class.mktmpdir do |path| + yielded_path = path + expect(path).to start_with('/tmp/d') + expect(described_class.exist?(path)).to be true + end + expect(described_class.exist?(yielded_path)).to be false + end + + it 'removes the directory even if an exception occurs' do + yielded_path = nil + expect do + described_class.mktmpdir do |path| + yielded_path = path + expect(described_class.exist?(path)).to be true + raise 'test exception' + end + end.to raise_error('test exception') + expect(described_class.exist?(yielded_path)).to be false + end + + it 'works with a prefix and block' do + yielded_path = nil + described_class.mktmpdir('test_') do |path| + yielded_path = path + expect(path).to start_with('/tmp/test_') + expect(described_class.exist?(path)).to be true + end + expect(described_class.exist?(yielded_path)).to be false + end + end + + it 'creates unique directory names' do + path1 = described_class.mktmpdir + path2 = described_class.mktmpdir + expect(path1).not_to eq(path2) + end + end + describe '.unlink' do subject { described_class } it_behaves_like 'aliased method', :unlink, :rmdir