Fix test-case and improve command-line interface

This commit is contained in:
Junegunn Choi 2014-04-21 23:29:03 +09:00
parent 501af62661
commit c980f9d3ce
2 changed files with 108 additions and 71 deletions

66
fzf
View File

@ -39,7 +39,6 @@
require 'thread' require 'thread'
require 'curses' require 'curses'
require 'set' require 'set'
require 'msgpack'
unless String.method_defined? :force_encoding unless String.method_defined? :force_encoding
class String class String
@ -77,11 +76,30 @@ class FZF
# TODO # TODO
def cores? def cores?
cores = `sysctl -n hw.physicalcpu 2> /dev/null`.chomp if File.readable? '/proc/cpuinfo'
if $?.exitstatus == 0 cores = Hash.new { |h, k| h[k] = Set.new }
cores.to_i proc_id = nil
set = Set.new
File.readlines('/proc/cpuinfo').each do |line|
case line
when /physical id/
if proc_id != line
cores[line] = set
set = Set.new
end
cores[proc_id = line] = set
when /core id/
set << line
end
end
cores.values.inject(0) { |sum, s| sum + (s.length || 1) }
else else
1 cores = `sysctl -n hw.physicalcpu 2> /dev/null`.chomp
if $?.exitstatus == 0
cores.to_i
else
1
end
end end
end end
@ -100,8 +118,8 @@ class FZF
@nth = nil @nth = nil
@delim = nil @delim = nil
@pids = [] @pids = []
@procs = cores? @procs = nil
@parallel_min = 10000 @par_min = 10000
argv = argv =
if opts = ENV['FZF_DEFAULT_OPTS'] if opts = ENV['FZF_DEFAULT_OPTS']
@ -168,15 +186,22 @@ class FZF
when /^-p([1-9][0-9]*)$/, /^--parallel=([1-9][0-9]*)$/ when /^-p([1-9][0-9]*)$/, /^--parallel=([1-9][0-9]*)$/
@procs = $1.to_i @procs = $1.to_i
when '--parallel-min' when '--parallel-min'
usage 1, 'number of processes required' unless pmin = argv.shift usage 1, 'number required' unless pmin = argv.shift
@parallel_min = pmin.to_i @par_min = pmin.to_i
when /^--parallel-min=([1-9][0-9]*)$/ when /^--parallel-min=([1-9][0-9]*)$/
@parallel_min = $1.to_i @par_min = $1.to_i
else else
usage 1, "illegal option: #{o}" usage 1, "illegal option: #{o}"
end end
end end
begin
require 'msgpack'
@procs ||= cores?
rescue LoadError
@procs = nil
end
@source = source.clone @source = source.clone
@mtx = Mutex.new @mtx = Mutex.new
@cv = ConditionVariable.new @cv = ConditionVariable.new
@ -305,6 +330,10 @@ class FZF
-n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting -n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting
search scope (positive or negative integers) search scope (positive or negative integers)
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
-p, --parallel=N Number of processes for parallel search
(default: auto-detect)
--parallel-min=N Minimum number of items to start parallel search
(default: 10000)
Search result Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000) -s, --sort=MAX Maximum number of matched items to sort (default: 1000)
@ -704,7 +733,7 @@ class FZF
def search lists, q, cx def search lists, q, cx
cache_count, cached = cached(lists, q, q[0, cx], q[cx..-1]) cache_count, cached = cached(lists, q, q[0, cx], q[cx..-1])
if @procs <= 1 || lists.empty? || lists.length < @procs || cache_count < @parallel_min if !@procs || @procs <= 1 || lists.empty? || lists.length < @procs || cache_count < @par_min
search_sequential lists, q, cached search_sequential lists, q, cached
else else
search_parallel lists, q, cached search_parallel lists, q, cached
@ -739,6 +768,7 @@ class FZF
slice_size = lists.length / @procs slice_size = lists.length / @procs
slices = lists.each_slice(slice_size) slices = lists.each_slice(slice_size)
render { print_info " (#{@procs}x)" }
triples = slices.map do |lists| triples = slices.map do |lists|
read, write = IO.pipe read, write = IO.pipe
[fork do [fork do
@ -766,16 +796,12 @@ class FZF
write.close write.close
result = read.read result = read.read
_, status = Process.wait2(pid) _, status = Process.wait2(pid)
raise if result.empty? skip = status.exitstatus != 0
if status.exitstatus == 0 MessagePack.unpack(result).each do |list_object_id, data|
MessagePack.unpack(result).each do |list_object_id, data| mutex.synchronize do
mutex.synchronize do matches.concat data
matches.concat data
end
matcher.cache list_map[list_object_id], q, data
end end
else matcher.cache list_map[list_object_id], q, data
skip = true
end end
end end
}.each(&:join) }.each(&:join)

View File

@ -220,7 +220,10 @@ class TestFZF < MiniTest::Unit::TestCase
["juiceless", [[0, 1]]], ["juiceless", [[0, 1]]],
["juicily", [[0, 1]]], ["juicily", [[0, 1]]],
["juiciness", [[0, 1]]], ["juiciness", [[0, 1]]],
["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort) ["juicy", [[0, 1]]]], matcher.match(list, 'j').sort)
assert matcher.caches.empty?
matcher.cache list, 'j', matcher.match(list, 'j')
assert !matcher.caches.empty? assert !matcher.caches.empty?
assert_equal [list.object_id], matcher.caches.keys assert_equal [list.object_id], matcher.caches.keys
assert_equal 1, matcher.caches[list.object_id].length assert_equal 1, matcher.caches[list.object_id].length
@ -228,11 +231,13 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal( assert_equal(
[["juicily", [[0, 5]]], [["juicily", [[0, 5]]],
["juiciness", [[0, 5]]]], matcher.match(list, 'jii', '', '').sort) ["juiciness", [[0, 5]]]], matcher.match(list, 'jii').sort)
matcher.cache list, 'jii', matcher.match(list, 'jii')
assert_equal( assert_equal(
[["juicily", [[2, 5]]], [["juicily", [[2, 5]]],
["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort) ["juiciness", [[2, 5]]]], matcher.match(list, 'ii').sort)
matcher.cache list, 'ii', matcher.match(list, 'jii')
assert_equal 3, matcher.caches[list.object_id].length assert_equal 3, matcher.caches[list.object_id].length
assert_equal 2, matcher.caches[list.object_id]['ii'].length assert_equal 2, matcher.caches[list.object_id]['ii'].length
@ -256,40 +261,40 @@ class TestFZF < MiniTest::Unit::TestCase
def test_fuzzy_matcher_case_sensitive def test_fuzzy_matcher_case_sensitive
# Smart-case match (Uppercase found) # Smart-case match (Uppercase found)
assert_equal [['Fruit', [[0, 5]]]], assert_equal [['Fruit', [[0, 5]]]],
FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'Fruit').sort
# Smart-case match (Uppercase not-found) # Smart-case match (Uppercase not-found)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'fruit', '', '').sort FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'fruit').sort
# Case-sensitive match (-i) # Case-sensitive match (-i)
assert_equal [['Fruit', [[0, 5]]]], assert_equal [['Fruit', [[0, 5]]]],
FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit').sort
# Case-insensitive match (+i) # Case-insensitive match (+i)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::FuzzyMatcher.new(Regexp::IGNORECASE). FZF::FuzzyMatcher.new(Regexp::IGNORECASE).
match(%w[Fruit Grapefruit], 'Fruit', '', '').sort match(%w[Fruit Grapefruit], 'Fruit').sort
end end
def test_extended_fuzzy_matcher_case_sensitive def test_extended_fuzzy_matcher_case_sensitive
%w['Fruit Fruit$].each do |q| %w['Fruit Fruit$].each do |q|
# Smart-case match (Uppercase found) # Smart-case match (Uppercase found)
assert_equal [['Fruit', [[0, 5]]]], assert_equal [['Fruit', [[0, 5]]]],
FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q, '', '').sort FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q).sort
# Smart-case match (Uppercase not-found) # Smart-case match (Uppercase not-found)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q.downcase, '', '').sort FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q.downcase).sort
# Case-sensitive match (-i) # Case-sensitive match (-i)
assert_equal [['Fruit', [[0, 5]]]], assert_equal [['Fruit', [[0, 5]]]],
FZF::ExtendedFuzzyMatcher.new(0).match(%w[Fruit Grapefruit], q, '', '').sort FZF::ExtendedFuzzyMatcher.new(0).match(%w[Fruit Grapefruit], q).sort
# Case-insensitive match (+i) # Case-insensitive match (+i)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::ExtendedFuzzyMatcher.new(Regexp::IGNORECASE). FZF::ExtendedFuzzyMatcher.new(Regexp::IGNORECASE).
match(%w[Fruit Grapefruit], q, '', '').sort match(%w[Fruit Grapefruit], q).sort
end end
end end
@ -304,7 +309,8 @@ class TestFZF < MiniTest::Unit::TestCase
juicy juicy
_juice] _juice]
match = proc { |q, prefix| match = proc { |q, prefix|
matcher.match(list, q, prefix, '').sort.map { |p| [p.first, p.last.sort] } cached = matcher.cached(list, q, prefix, '')
matcher.match(list, q, cached).sort.map { |p| [p.first, p.last.sort] }
} }
assert matcher.caches.empty? assert matcher.caches.empty?
@ -366,15 +372,20 @@ class TestFZF < MiniTest::Unit::TestCase
c.java$ c.java$
d.java d.java
] ]
match = lambda do |q, p|
cached = matcher.cached(list, q, p, '')
result = matcher.match(list, q, cached)
matcher.cache list, q, result
result
end
2.times do 2.times do
assert_equal 5, matcher.match(list, 'java', 'java', '').length assert_equal 5, match.call('java', 'java').length
assert_equal 3, matcher.match(list, 'java$', 'java$', '').length assert_equal 3, match.call('java$', 'java$').length
assert_equal 1, matcher.match(list, 'java$$', 'java$$', '').length assert_equal 1, match.call('java$$', 'java$$').length
assert_equal 0, match.call('!java', '!java').length
assert_equal 0, matcher.match(list, '!java', '!java', '').length assert_equal 4, match.call('!^jav', '!^jav').length
assert_equal 4, matcher.match(list, '!^jav', '!^jav', '').length assert_equal 4, match.call('!^java', '!^java').length
assert_equal 4, matcher.match(list, '!^java', '!^java', '').length assert_equal 2, match.call('!^java !b !c', '!^java').length
assert_equal 2, matcher.match(list, '!^java !b !c', '!^java', '').length
end end
end end
@ -400,7 +411,7 @@ class TestFZF < MiniTest::Unit::TestCase
["0____1", [[0, 6]]], ["0____1", [[0, 6]]],
["0_____1", [[0, 7]]], ["0_____1", [[0, 7]]],
["0______1", [[0, 8]]]], ["0______1", [[0, 8]]]],
FZF.sort(matcher.match(list, '01', '', ''))) FZF.sort(matcher.match(list, '01')))
assert_equal( assert_equal(
[["01", [[0, 1], [1, 2]]], [["01", [[0, 1], [1, 2]]],
@ -411,7 +422,7 @@ class TestFZF < MiniTest::Unit::TestCase
["____0_1", [[4, 5], [6, 7]]], ["____0_1", [[4, 5], [6, 7]]],
["0______1", [[0, 1], [7, 8]]], ["0______1", [[0, 1], [7, 8]]],
["___01___", [[3, 4], [4, 5]]]], ["___01___", [[3, 4], [4, 5]]]],
FZF.sort(xmatcher.match(list, '0 1', '', ''))) FZF.sort(xmatcher.match(list, '0 1')))
assert_equal( assert_equal(
[["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]], [["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]],
@ -420,7 +431,7 @@ class TestFZF < MiniTest::Unit::TestCase
["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]], ["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]],
["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]], ["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]],
["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]], ["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]],
FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple| FZF.sort(xmatcher.match(list, '01 __')).map { |tuple|
tuple << FZF.rank(tuple) tuple << FZF.rank(tuple)
} }
) )
@ -433,16 +444,16 @@ class TestFZF < MiniTest::Unit::TestCase
extended-exact-mode-not-fuzzy extended-exact-mode-not-fuzzy
extended'-fuzzy-mode extended'-fuzzy-mode
] ]
assert_equal 2, fuzzy.match(list, 'extended', '', '').length assert_equal 2, fuzzy.match(list, 'extended').length
assert_equal 2, fuzzy.match(list, 'mode extended', '', '').length assert_equal 2, fuzzy.match(list, 'mode extended').length
assert_equal 2, fuzzy.match(list, 'xtndd', '', '').length assert_equal 2, fuzzy.match(list, 'xtndd').length
assert_equal 2, fuzzy.match(list, "'-fuzzy", '', '').length assert_equal 2, fuzzy.match(list, "'-fuzzy").length
assert_equal 2, exact.match(list, 'extended', '', '').length assert_equal 2, exact.match(list, 'extended').length
assert_equal 2, exact.match(list, 'mode extended', '', '').length assert_equal 2, exact.match(list, 'mode extended').length
assert_equal 0, exact.match(list, 'xtndd', '', '').length assert_equal 0, exact.match(list, 'xtndd').length
assert_equal 1, exact.match(list, "'-fuzzy", '', '').length assert_equal 1, exact.match(list, "'-fuzzy").length
assert_equal 2, exact.match(list, "-fuzzy", '', '').length assert_equal 2, exact.match(list, "-fuzzy").length
end end
if RUBY_PLATFORM =~ /darwin/ if RUBY_PLATFORM =~ /darwin/
@ -472,16 +483,16 @@ class TestFZF < MiniTest::Unit::TestCase
def test_nfd_fuzzy_matcher def test_nfd_fuzzy_matcher
matcher = FZF::FuzzyMatcher.new 0 matcher = FZF::FuzzyMatcher.new 0
assert_equal [], matcher.match([NFD + NFD], '할', '', '') assert_equal [], matcher.match([NFD + NFD], '할')
match = matcher.match([NFD + NFD], '글글', '', '') match = matcher.match([NFD + NFD], '글글')
assert_equal [[NFD + NFD, [[3, 12]]]], match assert_equal [[NFD + NFD, [[3, 12]]]], match
assert_equal ['한글한글', [[1, 4]]], FZF::UConv.nfc(*match.first) assert_equal ['한글한글', [[1, 4]]], FZF::UConv.nfc(*match.first)
end end
def test_nfd_extended_fuzzy_matcher def test_nfd_extended_fuzzy_matcher
matcher = FZF::ExtendedFuzzyMatcher.new 0 matcher = FZF::ExtendedFuzzyMatcher.new 0
assert_equal [], matcher.match([NFD], "'글글", '', '') assert_equal [], matcher.match([NFD], "'글글")
match = matcher.match([NFD], "'한글", '', '') match = matcher.match([NFD], "'한글")
assert_equal [[NFD, [[0, 6]]]], match assert_equal [[NFD, [[0, 6]]]], match
assert_equal ['한글', [[0, 2]]], FZF::UConv.nfc(*match.first) assert_equal ['한글', [[0, 2]]], FZF::UConv.nfc(*match.first)
end end
@ -524,46 +535,46 @@ class TestFZF < MiniTest::Unit::TestCase
] ]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
assert_equal list, matcher.match(list, 'f', '', '').map(&:first) assert_equal list, matcher.match(list, 'f').map(&:first)
assert_equal [ assert_equal [
[list[0], [[2, 5]]], [list[0], [[2, 5]]],
[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') [list[1], [[9, 17]]]], matcher.match(list, 'is')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2] matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2]
assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f')
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '') assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3] matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3]
assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '') assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r')
# Comma-separated # Comma-separated
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3, 1] matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3, 1]
assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '') assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r')
# Ordered # Ordered
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1, 3] matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1, 3]
assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '') assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r')
regex = FZF.build_delim_regex "\t" regex = FZF.build_delim_regex "\t"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex
assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '') assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex
assert_equal [], matcher.match(list, 'r', '', '') assert_equal [], matcher.match(list, 'r')
assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is')
# Negative indexing # Negative indexing
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1], regex matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1], regex
assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '') assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt')
assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is')
# Regex delimiter # Regex delimiter
regex = FZF.build_delim_regex "[ \t]+" regex = FZF.build_delim_regex "[ \t]+"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex
assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first) assert_equal [list[1]], matcher.match(list, 'f').map(&:first)
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f')
end end
def stream_for str def stream_for str
@ -659,7 +670,7 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal [ assert_equal [
['1 2 3 4', [[0, 13], [16, 22]]], ['1 2 3 4', [[0, 13], [16, 22]]],
['1 3 4 2', [[0, 24], [12, 17]]], ['1 3 4 2', [[0, 24], [12, 17]]],
], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', '')) ], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34'))
end end
end end