findler 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZGNkZjllNWFjMzlhZjFlN2RjOGRkNWExMWU0NGU4ZDRiM2VhODhkZQ==
5
+ data.tar.gz: !binary |-
6
+ MTFiM2Y3Y2ZlMjk5MTRjMzBjYTQ0N2FmZTMyNTk3YjQ4MzA3NzhiMg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NWY4NDA4NzE2YzFkNWU0MWI5NjVkZGRhOTBhOWNiZjg0ZmY1NGJiYWI0MTll
10
+ YTcxMTg3N2I0ZjE2Y2RmODMxZTgyOTk0ZjUzOGQ0NzE2M2JhYmU3MDY1NjVh
11
+ NGYwNGZjMWRmNWEwOTAwMzhjODM1NDcxNWNkN2Q0MjI1MjJjZmE=
12
+ data.tar.gz: !binary |-
13
+ Njg4MDNmMTU4N2Q2NDc2NTcyZGU0ZWE2YjBhNGI1MmFjOGY4MTljNjQzMzY5
14
+ YTU5Yjk5NjBlYjc2MjI2Yzc4YmQxYTFiNTUyMDBhMzBmOWMxOGYyZDdjMjcy
15
+ NGY2NjI5YWQzNTM3MTJiMWJhZTgyMTg2MDBkMDE5NTRlYTNjMmM=
@@ -3,4 +3,7 @@ script: bundle exec rake
3
3
  rvm:
4
4
  - 1.8.7
5
5
  - 1.9.3
6
+ - 2.0.0
7
+ - jruby-18mode # JRuby in 1.8 mode
8
+ - jruby-19mode # JRuby in 1.9 mode
6
9
  - rbx-18mode
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source "http://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
  gemspec
3
3
 
data/README.md CHANGED
@@ -26,25 +26,17 @@ The entire state of the iteration for the filesystem is returned, which can then
26
26
  be pushed onto any durable storage, like ActiveRecord or Redis, or just a local file:
27
27
 
28
28
  ```ruby
29
- File.open('iterator.state', 'w') { |f| Marshal.dump(iterator, f) }
29
+ File.open('iterator.state', 'wb') { |f| Marshal.dump(iterator, f) }
30
30
  ```
31
31
 
32
32
  To resume iteration:
33
33
 
34
34
  ```ruby
35
- Marshal.load(IO.open('iterator.state'))
36
- iterator.next_file
35
+ iterator2 = Marshal.load(File.open('iterator.state', 'rb'))
36
+ iterator2.next_file
37
37
  # => "/Users/mrm/Photos/img_1001.jpg"
38
38
  ```
39
39
 
40
- To re-check a directory hierarchy for files that you haven't visited yet:
41
-
42
- ```ruby
43
- iterator.rescan!
44
- iterator.next_file
45
- # => "/Users/mrm/Photos/img_1002.jpg"
46
- ```
47
-
48
40
  External synchronization between the serialized state of the
49
41
  iterator and the other processes will have to be done by you, of course.
50
42
  The ```load```, ```next_file``` , and ```dump``` should be done while holding
@@ -123,6 +115,12 @@ Because procs and lambdas aren't ```Marshal```able, and I didn't want to use som
123
115
 
124
116
  ## Changelog
125
117
 
118
+ ### 0.0.7
119
+ * Use a non-inherited set per iterator, rather than a global bloom filter
120
+ * Removed the ability to "rescan" due to the weight of the bloom filter in marshalling when
121
+ traversing an enormous tree.
122
+ * Fixed marshal documentation and tests to support ruby 1.9+
123
+
126
124
  ### 0.0.6
127
125
  * ```add_filters``` takes an array, not a glob
128
126
  * added tests for order_by_mtime filters
@@ -1,12 +1,12 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "findler/version"
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'findler/version'
4
4
 
5
5
  Gem::Specification.new do |gem|
6
- gem.name = "findler"
6
+ gem.name = 'findler'
7
7
  gem.version = Findler::VERSION
8
- gem.authors = ["Matthew McEachen"]
9
- gem.email = ["matthew+github@mceachen.org"]
8
+ gem.authors = ['Matthew McEachen']
9
+ gem.email = %w(matthew+github@mceachen.org)
10
10
  gem.homepage = "https://github.com/mceachen/findler/"
11
11
  gem.summary = %q{Findler is a stateful filesystem iterator}
12
12
  gem.description = %q{Findler is designed for very large filesystem hierarchies,
@@ -17,9 +17,9 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = `git ls-files -- {test,features}/*`.split("\n")
18
18
  gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
19
19
  gem.require_paths = ["lib"]
20
- gem.add_development_dependency "rake"
21
- gem.add_development_dependency "yard"
22
- gem.add_development_dependency "minitest"
23
- gem.add_development_dependency "minitest-reporters"
24
- gem.add_dependency "bloomer"
20
+ gem.add_development_dependency 'rake'
21
+ gem.add_development_dependency 'yard'
22
+ gem.add_development_dependency 'minitest'
23
+ gem.add_development_dependency 'minitest-great_expectations'
24
+ gem.add_development_dependency 'minitest-reporters'
25
25
  end
@@ -1,19 +1,18 @@
1
- class Findler
2
-
3
- autoload :Iterator, "findler/iterator"
4
- autoload :Error, "findler/error"
5
- require "findler/filters"
1
+ require 'findler/error'
2
+ require 'findler/filters'
3
+ require 'findler/iterator'
4
+ require 'findler/path'
6
5
 
7
- IGNORE_CASE = 1
8
- INCLUDE_HIDDEN = 2
6
+ class Findler
7
+ attr_reader :path
9
8
 
10
9
  def initialize(path)
11
- @path = path
10
+ @path = Path.clean(path)
12
11
  @flags = 0
13
12
  end
14
13
 
15
- # These are File.fnmatch patterns.
16
- # If any pattern matches, it will be returned by Iterator#next.
14
+ # These are File.fnmatch patterns, and are only applied to files, not directories.
15
+ # If any pattern matches, it will be returned by Iterator#next_file.
17
16
  # (see File.fnmatch?)
18
17
  def patterns
19
18
  @patterns ||= []
@@ -38,31 +37,39 @@ class Findler
38
37
  # Should patterns be interpreted in a case-sensitive manner? The default is case sensitive,
39
38
  # but if your local filesystem is not case sensitive, this flag is a no-op.
40
39
  def case_sensitive!
41
- @flags &= ~IGNORE_CASE
40
+ @flags &= ~File::FNM_CASEFOLD
42
41
  end
43
42
 
44
43
  def case_insensitive!
45
- @flags |= IGNORE_CASE
44
+ @flags |= File::FNM_CASEFOLD
45
+ end
46
+
47
+ def ignore_case?
48
+ (@flags & File::FNM_CASEFOLD) > 0
46
49
  end
47
50
 
48
51
  # Should we traverse hidden directories and files? (default is to skip files that start
49
52
  # with a '.')
50
53
  def include_hidden!
51
- @flags |= INCLUDE_HIDDEN
54
+ @flags |= File::FNM_DOTMATCH
52
55
  end
53
56
 
54
57
  def exclude_hidden!
55
- @flags &= ~INCLUDE_HIDDEN
58
+ @flags &= ~File::FNM_DOTMATCH
56
59
  end
57
60
 
58
- def filter_class
59
- (@filter_class ||= Filters)
61
+ def include_hidden?
62
+ (@flags & File::FNM_DOTMATCH) > 0
60
63
  end
61
64
 
62
- def filter_class=(new_filter_class)
63
- raise Error unless new_filter_class.is_a? Class
64
- filters.each{|ea|new_filter_class.method(ea)} # verify the filters class has those methods defined
65
- @filter_class = new_filter_class
65
+ def filters_class
66
+ @filters_class ||= Filters
67
+ end
68
+
69
+ def filters_class=(new_filters_class)
70
+ raise Error unless new_filters_class.is_a? Class
71
+ filters.each { |ea| new_filters_class.method(ea) } # verify the filters class has those methods defined
72
+ @filters_class = new_filters_class
66
73
  end
67
74
 
68
75
  # Accepts symbols whose names are class methods on Finder::Filters.
@@ -78,12 +85,11 @@ class Findler
78
85
  # Note that the last filter added will be last to order the children, so it will be the
79
86
  # "primary" sort criterion.
80
87
  def add_filter(filter_symbol)
81
- filter_class.method(filter_symbol)
82
88
  filters << filter_symbol
83
89
  end
84
90
 
85
91
  def filters
86
- (@filters ||= [])
92
+ @filters ||= []
87
93
  end
88
94
 
89
95
  def add_filters(filter_symbols)
@@ -91,14 +97,41 @@ class Findler
91
97
  end
92
98
 
93
99
  def iterator
94
- Iterator.new(:path => @path,
95
- :patterns => @patterns,
96
- :flags => @flags,
97
- :filters => @filters)
100
+ Iterator.new(self, path)
98
101
  end
99
102
 
100
103
  private
101
104
 
105
+ def filter_paths(pathnames)
106
+ viable_paths = pathnames.select { |ea| viable_path?(ea) }
107
+ filters.inject(viable_paths) do |paths, filter_symbol|
108
+ apply_filter(paths, filter_symbol)
109
+ end
110
+ end
111
+
112
+ # Should the given file or directory be iterated over?
113
+ def viable_path?(pathname)
114
+ return false if !pathname.exist?
115
+ return false if !include_hidden? && Path.hidden?(pathname)
116
+ if patterns.empty? || pathname.directory?
117
+ true
118
+ else
119
+ patterns.any? { |p| pathname.fnmatch(p, @flags) }
120
+ end
121
+ end
122
+
123
+ def apply_filter(pathnames, filter_method_sym)
124
+ filtered_pathnames = filters_class.send(filter_method_sym, pathnames.dup)
125
+ unless filtered_pathnames.respond_to? :map
126
+ raise Error, "#{filters_class}.#{filter_method_sym} did not return an Enumerable"
127
+ end
128
+ unexpected_paths = filtered_pathnames - pathnames
129
+ unless unexpected_paths.empty?
130
+ raise Error, "#{filters_class}.#{filter_method_sym} returned unexpected paths: #{unexpected_paths.collect { |ea| ea.to_s }.join(",")}"
131
+ end
132
+ filtered_pathnames
133
+ end
134
+
102
135
  def normalize_extension(extension)
103
136
  if extension.nil? || extension.empty? || extension.start_with?(".")
104
137
  extension
@@ -1,2 +1,4 @@
1
- class Findler::Error < StandardError
1
+ class Findler
2
+ class Error < StandardError
3
+ end
2
4
  end
@@ -1,38 +1,40 @@
1
- class Findler::Filters
2
- # files first, then directories
3
- def self.files_first(paths)
4
- preserve_sort_by(paths) { |ea| ea.file? ? -1 : 1 }
5
- end
1
+ class Findler
2
+ class Filters
3
+ # files first, then directories
4
+ def self.files_first(paths)
5
+ preserve_sort_by(paths) { |ea| ea.file? ? -1 : 1 }
6
+ end
6
7
 
7
- # directories first, then files
8
- def self.directories_first(paths)
9
- preserve_sort_by(paths) { |ea| ea.directory? ? -1 : 1 }
10
- end
8
+ # directories first, then files
9
+ def self.directories_first(paths)
10
+ preserve_sort_by(paths) { |ea| ea.directory? ? -1 : 1 }
11
+ end
11
12
 
12
- # order by the mtime of each file. Oldest files first.
13
- def self.order_by_mtime_asc(paths)
14
- preserve_sort_by(paths) { |ea| ea.mtime.to_i }
15
- end
13
+ # order by the mtime of each file. Oldest files first.
14
+ def self.order_by_mtime_asc(paths)
15
+ preserve_sort_by(paths) { |ea| ea.mtime.to_i }
16
+ end
16
17
 
17
- # reverse order by the mtime of each file. Newest files first.
18
- def self.order_by_mtime_desc(paths)
19
- preserve_sort_by(paths) { |ea| -1 * ea.mtime.to_i }
20
- end
18
+ # reverse order by the mtime of each file. Newest files first.
19
+ def self.order_by_mtime_desc(paths)
20
+ preserve_sort_by(paths) { |ea| -1 * ea.mtime.to_i }
21
+ end
21
22
 
22
- # order by the name of each file.
23
- def self.order_by_name(paths)
24
- preserve_sort_by(paths) { |ea| ea.basename.to_s }
25
- end
23
+ # order by the name of each file.
24
+ def self.order_by_name(paths)
25
+ preserve_sort_by(paths) { |ea| ea.basename.to_s }
26
+ end
26
27
 
27
- # reverse the order of the sort
28
- def self.reverse(paths)
29
- paths.reverse
30
- end
28
+ # reverse the order of the sort
29
+ def self.reverse(paths)
30
+ paths.reverse
31
+ end
31
32
 
32
- def self.preserve_sort_by(array, &block)
33
- ea_to_index = Hash[array.zip((0..array.size-1).to_a)]
34
- array.sort_by do |ea|
35
- [yield(ea), ea_to_index[ea]]
33
+ def self.preserve_sort_by(array, &block)
34
+ ea_to_index = Hash[array.zip((0..array.size-1).to_a)]
35
+ array.sort_by do |ea|
36
+ [yield(ea), ea_to_index[ea]]
37
+ end
36
38
  end
37
39
  end
38
40
  end
@@ -1,56 +1,17 @@
1
- require 'bloomer'
2
- require 'pathname'
1
+ require 'set'
2
+ require 'forwardable'
3
3
 
4
4
  class Findler
5
-
6
5
  class Iterator
6
+ extend Forwardable
7
+ def_delegators :@configuration, :filter_paths
8
+ attr_reader :path
7
9
 
8
- attr_reader :path, :parent, :patterns, :flags, :filters_class, :filters, :visited_dirs, :visited_files
9
-
10
- def initialize(attrs, parent = nil)
11
- @path = attrs[:path]
12
- @path = Pathname.new(@path) unless @path.is_a? Pathname
13
- @path = @path.expand_path unless @path.absolute?
14
- @parent = parent
15
-
16
- set_inheritable_ivar(:visited_dirs, attrs) { self.class.new_presence_collection }
17
- set_inheritable_ivar(:visited_files, attrs) { self.class.new_presence_collection }
18
- set_inheritable_ivar(:patterns, attrs) { nil }
19
- set_inheritable_ivar(:flags, attrs) { 0 }
20
- set_inheritable_ivar(:filters, attrs) { [] }
21
- set_inheritable_ivar(:filters_class, attrs) { Filters }
22
- set_inheritable_ivar(:sort_with, attrs) { nil }
23
-
24
- @sub_iter = self.class.new(attrs[:sub_iter], self) if attrs[:sub_iter]
25
- end
26
-
27
- # Visit this directory and all sub directories, and check for unseen files. Only call on the root iterator.
28
- def rescan!
29
- raise Error, "Only invoke on root" unless @parent.nil?
30
- @visited_dirs = self.class.new_presence_collection
31
- @children = nil
32
- @sub_iter = nil
33
- end
34
-
35
- def ignore_case?
36
- (Findler::IGNORE_CASE & flags) > 0
37
- end
38
-
39
- def include_hidden?
40
- (Findler::INCLUDE_HIDDEN & flags) > 0
41
- end
42
-
43
- def fnmatch_flags
44
- @fnmatch_flags ||= (@parent && @parent.fnmatch_flags) || begin
45
- f = 0
46
- f |= File::FNM_CASEFOLD if ignore_case?
47
- f |= File::FNM_DOTMATCH if include_hidden?
48
- f
49
- end
50
- end
51
-
52
- def path
53
- @path
10
+ def initialize(findler, path, parent_iterator = nil)
11
+ @configuration = findler
12
+ @path = Path.clean(path).freeze
13
+ @parent = parent_iterator
14
+ @visited = []
54
15
  end
55
16
 
56
17
  def next_file
@@ -59,76 +20,40 @@ class Findler
59
20
  if @sub_iter
60
21
  nxt = @sub_iter.next_file
61
22
  return nxt unless nxt.nil?
62
- @visited_dirs.add @sub_iter.path.to_s
23
+ mark_visited(@sub_iter.path)
63
24
  @sub_iter = nil
64
25
  end
65
26
 
66
- # If someone touches the directory while we iterate, redo the @children.
67
- @children = nil if @path.ctime != @ctime || @path.mtime != @mtime
68
-
69
- if @children.nil?
70
- @mtime = @path.mtime
71
- @ctime = @path.ctime
72
- children = @path.children.delete_if { |ea| skip?(ea) }
73
- filtered_children = @filters.inject(children){ |c, f| filter(c, f) }
74
- @children = filtered_children
75
- end
76
-
77
- nxt = @children.shift
27
+ nxt = next_visitable_child
78
28
  return nil if nxt.nil?
79
29
 
80
30
  if nxt.directory?
81
- @sub_iter = Iterator.new({:path => nxt}, self)
31
+ @sub_iter = Iterator.new(@configuration, nxt, self)
82
32
  self.next_file
83
33
  else
84
- @visited_files.add nxt.to_s
34
+ mark_visited(nxt)
85
35
  nxt
86
36
  end
87
37
  end
88
38
 
89
39
  private
90
40
 
91
- def self.new_presence_collection
92
- Bloomer::Scalable.new(256, 1.0/1_000_000)
93
- end
94
-
95
- def filter(children, filter_symbol)
96
- filtered_children = filters_class.send(filter_symbol, children)
97
- unless filtered_children.respond_to? :collect
98
- raise Error, "#{path.to_s}: filter_with, must return an Enumerable"
99
- end
100
- children_as_pathnames = filtered_children.collect { |ea| ea.is_a?(Pathname) ? ea : Pathname.new(ea) }
101
- illegal_children = children_as_pathnames - children
102
- unless illegal_children.empty?
103
- raise Error, "#{path.to_s}: filter_with returned unexpected paths: #{illegal_children.join(",")}"
41
+ def children
42
+ # If someone touches the directory while we iterate, redo the @children.
43
+ if @children.nil? || @mtime != @path.mtime || @ctime != @path.ctime
44
+ @mtime = @path.mtime
45
+ @ctime = @path.ctime
46
+ @children = filter_paths(@path.children)
104
47
  end
105
- children_as_pathnames
48
+ @children
106
49
  end
107
50
 
108
- # Sets the instance variable to the value in attrs[field].
109
- # If attrs is missing a value, pull the value from the parent.
110
- # If the parent doesn't have a value, use the block to generate a default.
111
- def set_inheritable_ivar(field, attrs, &block)
112
- v = attrs[field]
113
- sym = "@#{field}".to_sym
114
- v ||= parent.instance_variable_get(sym)
115
- v ||= yield
116
- instance_variable_set(sym, v)
51
+ def next_visitable_child
52
+ children.detect { |ea| !@visited.include?(Path.base(ea)) }
117
53
  end
118
54
 
119
- def hidden?(pathname)
120
- pathname.basename.to_s.start_with?(".")
121
- end
122
-
123
- def skip? pathname
124
- s = pathname.to_s
125
- return true if !include_hidden? && hidden?(pathname)
126
- return visited_dirs.include?(s) if pathname.directory?
127
- return true if visited_files.include?(s)
128
- unless patterns.nil?
129
- return true if patterns.none? { |p| pathname.fnmatch(p, fnmatch_flags) }
130
- end
131
- return false
55
+ def mark_visited(path)
56
+ @visited << Path.base(path)
132
57
  end
133
58
  end
134
59
  end
@@ -0,0 +1,27 @@
1
+ require 'pathname'
2
+
3
+ class Findler
4
+ class Path
5
+ def self.clean(path)
6
+ path = Pathname.new(path) unless path.is_a? Pathname
7
+ path = path.expand_path unless path.absolute?
8
+ path
9
+ end
10
+
11
+ def self.base(path)
12
+ path.basename.to_s
13
+ end
14
+
15
+ def self.clean_array(array)
16
+ array.map { |ea| clean(ea) }
17
+ end
18
+
19
+ def self.base_array(array)
20
+ array.map { |ea| base(ea) }
21
+ end
22
+
23
+ def self.hidden?(path)
24
+ base(path).start_with?('.')
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  class Findler
2
- VERSION = "0.0.6"
3
- end
2
+ VERSION = Gem::Version.new("0.0.7")
3
+ end
@@ -9,7 +9,7 @@ class Findler::Filters
9
9
  end
10
10
 
11
11
  def self.invalid_return(children)
12
- "/invalid/file"
12
+ Pathname.new('/invalid/file')
13
13
  end
14
14
 
15
15
  def self.files_to_s(children)
@@ -23,107 +23,90 @@ describe Findler do
23
23
  `mkdir .hide ; touch .outer-hide dir-0/.hide .hide/normal.txt .hide/.secret`
24
24
  end
25
25
 
26
- it "should detect hidden files properly" do
27
- i = Findler::Iterator.new(:path => "/tmp")
28
- i.send(:hidden?, Pathname.new("/a/b")).must_equal false
29
- i.send(:hidden?, Pathname.new("/a/.b")).must_equal true
30
- i.send(:hidden?, Pathname.new("/.a/.b")).must_equal true
31
- i.send(:hidden?, Pathname.new("/.a/b")).must_equal false
32
- end
33
-
34
- it "should skip hidden files by default" do
35
- i = Findler.new("/tmp").iterator
36
- i.send(:skip?, Pathname.new("/tmp/not-hidden")).must_equal false
37
- i.send(:skip?, Pathname.new("/tmp/.hidden")).must_equal true
26
+ it 'detects hidden files properly' do
27
+ %w(/a/b /.a/b).each do |ea|
28
+ p = Pathname.new(ea)
29
+ Findler::Path.hidden?(p).must_be_false
30
+ end
31
+ %w(/a/.b /a/.b).each do |ea|
32
+ p = Pathname.new(ea)
33
+ Findler::Path.hidden?(p).must_be_true
34
+ end
38
35
  end
39
36
 
40
- it "should not skip hidden files when set" do
41
- f = Findler.new("/tmp")
42
- f.include_hidden!
43
- i = f.iterator
44
- i.send(:skip?, Pathname.new("/tmp/not-hidden")).must_equal false
45
- i.send(:skip?, Pathname.new("/tmp/.hidden")).must_equal false
37
+ it 'skips hidden files by default' do
38
+ with_tmp_dir do |dir|
39
+ visible = (dir + rand_alphanumeric).tap { |ea| ea.touch }
40
+ hidden = (dir + ".#{rand_alphanumeric}").tap { |ea| ea.touch }
41
+ f = Findler.new(dir)
42
+ f.send(:viable_path?, visible).must_equal true
43
+ f.send(:viable_path?, hidden).must_equal false
44
+ f.include_hidden!
45
+ f.send(:viable_path?, visible).must_equal true
46
+ f.send(:viable_path?, hidden).must_equal true
47
+ end
46
48
  end
47
49
 
48
- it "should find all non-hidden files by default" do
50
+ it 'finds all non-hidden files by default' do
49
51
  with_tree(%W(.jpg .txt)) do |dir|
50
52
  touch_secrets
51
53
  f = Findler.new(dir)
52
- collect_files(f.iterator).sort.must_equal `find * -type f -not -name '.*'`.split.sort
54
+ collect_files(f.iterator).must_equal_contents `find * -type f -not -name '.*'`.split
53
55
  f.exclude_hidden! # should be a no-op
54
- collect_files(f.iterator).sort.must_equal `find * -type f -not -name '.*'`.split.sort
56
+ collect_files(f.iterator).must_equal_contents `find * -type f -not -name '.*'`.split
55
57
  f.include_hidden!
56
- collect_files(f.iterator).sort.must_equal `find . -type f | sed -e 's/^\\.\\///'`.split.sort
58
+ collect_files(f.iterator).must_equal_contents `find . -type f | sed -e 's/^\\.\\///'`.split
57
59
  end
58
60
  end
59
61
 
60
- it "should find only .jpg files when constrained" do
62
+ it 'finds only .jpg files when constrained' do
61
63
  with_tree(%W(.jpg .txt .JPG)) do |dir|
62
64
  f = Findler.new(dir)
63
65
  f.add_extension ".jpg"
64
66
  if fs_case_sensitive?
65
67
  f.case_sensitive!
66
- collect_files(f.iterator).sort.must_equal `find * -type f -name \\*.jpg`.split.sort
68
+ collect_files(f.iterator).must_equal_contents `find * -type f -name \\*.jpg`.split
67
69
  end
68
70
  f.case_insensitive!
69
- collect_files(f.iterator).sort.must_equal `find * -type f -iname \\*.jpg`.split.sort
71
+ collect_files(f.iterator).must_equal_contents `find * -type f -iname \\*.jpg`.split
70
72
  end
71
73
  end
72
74
 
73
- it "should find .jpg or .JPG files when constrained" do
74
- with_tree(%W(.jpg .txt .JPG)) do |dir|
75
+ it 'finds .jpg or .JPG files when constrained' do
76
+ with_tree(%w(.jpg .txt .JPG)) do |dir|
75
77
  f = Findler.new(dir)
76
- f.add_extension ".jpg"
78
+ f.add_extension '.jpg'
77
79
  f.case_insensitive!
78
80
  iter = f.iterator
79
- collect_files(iter).sort.must_equal `find * -type f -iname \\*.jpg`.split.sort
81
+ collect_files(iter).must_equal_contents `find * -type f -iname \\*.jpg`.split
80
82
  end
81
83
  end
82
84
 
83
- it "should find files added after iteration started" do
85
+ it 'finds files added after iteration started' do
84
86
  with_tree(%W(.txt)) do |dir|
85
87
  f = Findler.new(dir)
86
88
  iter = f.iterator
87
89
  iter.next_file.wont_be_nil
88
-
89
- # cheating with mtime on the touch doesn't properly update the parent directory ctime,
90
- # so we have to deal with the second-granularity resolution of the filesystem.
91
- sleep(1.1)
92
-
93
- FileUtils.touch(dir + "new.txt")
94
- collect_files(iter).must_include("new.txt")
95
- end
96
- end
97
-
98
- it "should find new files after a rescan" do
99
- with_tree([".txt", ".no"]) do |dir|
100
- f = Findler.new(dir)
101
- f.add_extension ".txt"
102
- iter = f.iterator
103
- collect_files(iter).sort.must_equal `find * -type f -iname \\*.txt`.split.sort
104
- FileUtils.touch(dir + "dir-0" + "dir-1" + "new-0.txt")
105
- FileUtils.touch(dir + "dir-1" + "dir-0" + "new-1.txt")
106
- FileUtils.touch(dir + "dir-2" + "dir-2" + "new-2.txt")
107
- collect_files(iter).must_be_empty
108
- iter.rescan!
109
- collect_files(iter).sort.must_equal ["dir-0/dir-1/new-0.txt", "dir-1/dir-0/new-1.txt", "dir-2/dir-2/new-2.txt"]
90
+ sleep(1.1) # <- deal with the second-granularity resolution of the filesystem
91
+ (dir + 'new.txt').touch
92
+ collect_files(iter).must_include('new.txt')
110
93
  end
111
94
  end
112
95
 
113
- it "should not return files removed after iteration started" do
114
- with_tree([".txt"]) do |dir|
96
+ it 'should not return files removed after iteration started' do
97
+ with_tree(%w(.txt)) do |dir|
115
98
  f = Findler.new(dir)
116
99
  iter = f.iterator
117
100
  iter.next_file.wont_be_nil
118
- sleep(1.1) # see above for hand-wringing-defense of this atrocity
119
-
120
- (dir + "tmp-1.txt").unlink
101
+ sleep(1.1) # < make sure mtime change will be detected (which only has second resolution)
102
+ (dir + 'tmp-1.txt').unlink
121
103
  collect_files(iter).wont_include("tmp-1.txt")
122
104
  end
123
105
  end
124
106
 
125
- it "should dump/load in the middle of iterating" do
126
- with_tree(%W(.jpg .txt .JPG)) do |dir|
107
+
108
+ it 'dump/loads in the middle of iterating' do
109
+ with_tree(%w(.jpg .txt .JPG)) do |dir|
127
110
  all_files = `find * -type f -iname \\*.jpg`.split
128
111
  all_files.size.times do |i|
129
112
  f = Findler.new(dir)
@@ -131,19 +114,24 @@ describe Findler do
131
114
  f.case_insensitive!
132
115
  iter_a = f.iterator
133
116
  files_a = i.times.collect { relative_path(iter_a.path, iter_a.next_file) }
134
- iter_b = Marshal.load(Marshal.dump(iter_a))
117
+ iter_b = marshal_round_trip(iter_a)
135
118
  files_b = collect_files(iter_b)
136
119
 
137
- iter_c = Marshal.load(Marshal.dump(iter_b))
138
- collect_files(iter_c)
139
- iter_c.next_file.must_be_nil
120
+ files_a.wont_include_any files_b
121
+ files_b.wont_include_any files_a
122
+ (files_a + files_b).must_equal_contents all_files
123
+
124
+ # iter_b should be "exhausted" now.
125
+ collect_files(iter_b).must_be_empty
140
126
 
141
- (files_a + files_b).sort.must_equal all_files.sort
127
+ # and "exhaustion" should survive marshalling:
128
+ iter_c = marshal_round_trip(iter_b)
129
+ collect_files(iter_c).must_be_empty
142
130
  end
143
131
  end
144
132
  end
145
133
 
146
- it "should create an iterator even for a non-existent directory" do
134
+ it 'creates an iterator even for a non-existent directory' do
147
135
  tmpdir = nil
148
136
  Dir.mktmpdir do |dir|
149
137
  tmpdir = Pathname.new dir
@@ -153,7 +141,7 @@ describe Findler do
153
141
  collect_files(f.iterator).must_be_empty
154
142
  end
155
143
 
156
- it "should raise an error if the block given to next_file returns nil" do
144
+ it 'raises an error if the block given to next_file returns nil' do
157
145
  Dir.mktmpdir do |dir|
158
146
  f = Findler.new(dir)
159
147
  f.add_filter :no_return
@@ -162,7 +150,7 @@ describe Findler do
162
150
  end
163
151
  end
164
152
 
165
- it "should raise an error if the block returns non-children" do
153
+ it 'raises an error if the block returns non-children' do
166
154
  with_tree(%W(.txt)) do |dir|
167
155
  f = Findler.new(dir)
168
156
  f.add_filter :invalid_return
@@ -171,32 +159,31 @@ describe Findler do
171
159
  end
172
160
  end
173
161
 
174
- it "should support filter_with against global/Kernel methods" do
162
+ it 'raises error when filter methods return strings' do
175
163
  with_tree(%W(.txt)) do |dir|
176
164
  f = Findler.new(dir)
177
165
  f.add_filter :files_to_s
178
- iter = f.iterator
179
- files = collect_files(iter)
180
- files.sort.must_equal `find * -type f`.split.sort
166
+ i = f.iterator
167
+ lambda { i.next_file }.must_raise(Findler::Error)
181
168
  end
182
169
  end
183
170
 
184
- it "should support next_file blocks properly" do
171
+ it 'supports next_file blocks properly' do
185
172
  with_tree(%W(.a .b)) do |dir|
186
173
  Dir["**/*.a"].each { |ea| File.open(ea, 'w') { |f| f.write("hello") } }
187
174
  f = Findler.new(dir)
188
175
  f.add_filter :non_empty_files
189
176
  iter = f.iterator
190
177
  files = collect_files(iter)
191
- files.sort.must_equal `find * -type f -name \\*.a`.split.sort
178
+ files.must_equal_contents `find * -type f -name \\*.a`.split
192
179
  end
193
180
  end
194
181
 
195
- it "should support files_first ordering" do
182
+ it 'supports files_first ordering' do
196
183
  with_tree(%W(.a), {
197
- :depth => 2,
198
- :files_per_dir => 2,
199
- :subdirs_per_dir => 1,
184
+ :depth => 2,
185
+ :files_per_dir => 2,
186
+ :subdirs_per_dir => 1,
200
187
  }) do |dir|
201
188
  f = Findler.new(dir)
202
189
  f.add_filters([:order_by_name, :files_first])
@@ -207,11 +194,11 @@ describe Findler do
207
194
  end
208
195
  end
209
196
 
210
- it "should support directory_first ordering" do
197
+ it 'supports directory_first ordering' do
211
198
  with_tree(%W(.a), {
212
- :depth => 2,
213
- :files_per_dir => 2,
214
- :subdirs_per_dir => 1,
199
+ :depth => 2,
200
+ :files_per_dir => 2,
201
+ :subdirs_per_dir => 1,
215
202
  }) do |dir|
216
203
  f = Findler.new(dir)
217
204
  f.add_filters([:order_by_name, :directories_first])
@@ -1,11 +1,13 @@
1
- require 'minitest/spec'
2
- require 'minitest/reporters'
3
1
  require 'minitest/autorun'
2
+ require 'minitest/great_expectations'
4
3
  require 'tmpdir'
5
4
  require 'fileutils'
6
5
  require 'findler'
7
6
 
8
- MiniTest::Reporters.use!
7
+ unless ENV['CI']
8
+ require 'minitest/reporters'
9
+ MiniTest::Reporters.use!
10
+ end
9
11
 
10
12
  def with_tmp_dir(&block)
11
13
  cwd = Dir.pwd
@@ -27,20 +29,20 @@ end
27
29
 
28
30
  def mk_tree(target_dir, options)
29
31
  opts = {
30
- :depth => 3,
31
- :files_per_dir => 3,
32
- :subdirs_per_dir => 3,
33
- :prefix => "tmp",
34
- :suffix => "",
35
- :dir_prefix => "dir",
36
- :dir_suffix => ""
32
+ :depth => 3,
33
+ :files_per_dir => 3,
34
+ :subdirs_per_dir => 3,
35
+ :prefix => 'tmp',
36
+ :suffix => '',
37
+ :dir_prefix => 'dir',
38
+ :dir_suffix => ''
37
39
  }.merge options
38
40
  p = target_dir.is_a?(Pathname) ? target_dir : Pathname.new(target_dir)
39
41
  p.mkdir unless p.exist?
40
42
 
41
43
  opts[:files_per_dir].times do |i|
42
44
  fname = "#{opts[:prefix]}-#{i}#{opts[:suffix]}"
43
- FileUtils.touch(p + fname).to_s
45
+ (p + fname).touch
44
46
  end
45
47
  return if (opts[:depth] -= 1) <= 0
46
48
  opts[:subdirs_per_dir].times do |i|
@@ -58,9 +60,15 @@ def relative_path(parent, pathname)
58
60
  pathname.relative_path_from(parent).to_s
59
61
  end
60
62
 
63
+ def marshal_round_trip(iter)
64
+ output = "#{rand_alphanumeric}.ser"
65
+ File.open(output, 'wb') { |io| Marshal.dump(iter, io) }
66
+ Marshal.load(File.open(output, 'rb'))
67
+ end
68
+
61
69
  def collect_files(iter)
62
70
  files = []
63
- while nxt = iter.next_file
71
+ while (nxt = iter.next_file)
64
72
  files << relative_path(iter.path, nxt)
65
73
  end
66
74
  files
@@ -68,16 +76,25 @@ end
68
76
 
69
77
  def fs_case_sensitive?
70
78
  @fs_case_sensitive ||= begin
71
- `touch CASETEST`
72
- !File.exist?('casetest')
79
+ downcase = Pathname.new(rand_alphanumeric.downcase)
80
+ downcase.touch
81
+ upcase = Pathname.new(downcase.basename.to_s.upcase)
82
+ !upcase.exist?
73
83
  ensure
74
- `rm CASETEST`
75
- end
84
+ downcase.unlink
85
+ end.tap { |ea| puts "fs_case_sensitive = #{ea}" }
76
86
  end
77
87
 
78
- ALPHANUMERIC = (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a).freeze
88
+ ALPHANUMERIC = (('a'..'z').to_a + ('0'..'9').to_a).freeze
89
+
79
90
  def rand_alphanumeric(length = 10)
80
91
  (0..length).collect do
81
92
  ALPHANUMERIC[rand(ALPHANUMERIC.length)]
82
93
  end.join
83
94
  end
95
+
96
+ class Pathname
97
+ def touch
98
+ FileUtils.touch(self.expand_path.to_s)
99
+ end
100
+ end
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: findler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
5
- prerelease:
4
+ version: 0.0.7
6
5
  platform: ruby
7
6
  authors:
8
7
  - Matthew McEachen
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-12-30 00:00:00.000000000 Z
11
+ date: 2013-07-05 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rake
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ! '>='
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ! '>='
28
25
  - !ruby/object:Gem::Version
@@ -30,7 +27,6 @@ dependencies:
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: yard
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
31
  - - ! '>='
36
32
  - !ruby/object:Gem::Version
@@ -38,7 +34,6 @@ dependencies:
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
38
  - - ! '>='
44
39
  - !ruby/object:Gem::Version
@@ -46,7 +41,6 @@ dependencies:
46
41
  - !ruby/object:Gem::Dependency
47
42
  name: minitest
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
45
  - - ! '>='
52
46
  - !ruby/object:Gem::Version
@@ -54,15 +48,13 @@ dependencies:
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
52
  - - ! '>='
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
62
55
  - !ruby/object:Gem::Dependency
63
- name: minitest-reporters
56
+ name: minitest-great_expectations
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
59
  - - ! '>='
68
60
  - !ruby/object:Gem::Version
@@ -70,23 +62,20 @@ dependencies:
70
62
  type: :development
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
66
  - - ! '>='
76
67
  - !ruby/object:Gem::Version
77
68
  version: '0'
78
69
  - !ruby/object:Gem::Dependency
79
- name: bloomer
70
+ name: minitest-reporters
80
71
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
72
  requirements:
83
73
  - - ! '>='
84
74
  - !ruby/object:Gem::Version
85
75
  version: '0'
86
- type: :runtime
76
+ type: :development
87
77
  prerelease: false
88
78
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
79
  requirements:
91
80
  - - ! '>='
92
81
  - !ruby/object:Gem::Version
@@ -112,39 +101,33 @@ files:
112
101
  - lib/findler/error.rb
113
102
  - lib/findler/filters.rb
114
103
  - lib/findler/iterator.rb
104
+ - lib/findler/path.rb
115
105
  - lib/findler/version.rb
116
106
  - test/filters_test.rb
117
107
  - test/findler_test.rb
118
108
  - test/minitest_helper.rb
119
109
  homepage: https://github.com/mceachen/findler/
120
110
  licenses: []
111
+ metadata: {}
121
112
  post_install_message:
122
113
  rdoc_options: []
123
114
  require_paths:
124
115
  - lib
125
116
  required_ruby_version: !ruby/object:Gem::Requirement
126
- none: false
127
117
  requirements:
128
118
  - - ! '>='
129
119
  - !ruby/object:Gem::Version
130
120
  version: '0'
131
- segments:
132
- - 0
133
- hash: 1051954001939010091
134
121
  required_rubygems_version: !ruby/object:Gem::Requirement
135
- none: false
136
122
  requirements:
137
123
  - - ! '>='
138
124
  - !ruby/object:Gem::Version
139
125
  version: '0'
140
- segments:
141
- - 0
142
- hash: 1051954001939010091
143
126
  requirements: []
144
127
  rubyforge_project:
145
- rubygems_version: 1.8.23
128
+ rubygems_version: 2.0.3
146
129
  signing_key:
147
- specification_version: 3
130
+ specification_version: 4
148
131
  summary: Findler is a stateful filesystem iterator
149
132
  test_files:
150
133
  - test/filters_test.rb