droxi 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/Rakefile +18 -5
- data/bin/droxi +2 -3
- data/droxi.gemspec +2 -2
- data/lib/droxi.rb +45 -33
- data/lib/droxi/commands.rb +117 -97
- data/lib/droxi/complete.rb +36 -31
- data/lib/droxi/settings.rb +51 -52
- data/lib/droxi/state.rb +72 -69
- data/lib/droxi/text.rb +27 -37
- data/spec/all.rb +10 -0
- data/spec/commands_spec.rb +285 -114
- data/spec/complete_spec.rb +93 -28
- data/spec/settings_spec.rb +37 -4
- data/spec/state_spec.rb +51 -23
- data/spec/testutils.rb +65 -0
- data/spec/text_spec.rb +5 -5
- metadata +3 -2
data/spec/complete_spec.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'dropbox_sdk'
|
2
2
|
require 'minitest/autorun'
|
3
3
|
|
4
|
+
require_relative 'testutils'
|
5
|
+
require_relative '../lib/droxi/commands'
|
4
6
|
require_relative '../lib/droxi/complete'
|
5
7
|
require_relative '../lib/droxi/settings'
|
6
8
|
require_relative '../lib/droxi/state'
|
@@ -12,99 +14,162 @@ describe Complete do
|
|
12
14
|
rand(length).times.map { CHARACTERS.sample }.join
|
13
15
|
end
|
14
16
|
|
15
|
-
describe
|
16
|
-
it
|
17
|
+
describe 'when resolving a local search path' do
|
18
|
+
it 'must resolve unqualified string to working directory' do
|
17
19
|
Complete.local_search_path('').must_equal Dir.pwd
|
18
20
|
Complete.local_search_path('f').must_equal Dir.pwd
|
19
21
|
end
|
20
22
|
|
21
|
-
it
|
23
|
+
it 'must resolve / to root directory' do
|
22
24
|
Complete.local_search_path('/').must_equal '/'
|
23
25
|
Complete.local_search_path('/f').must_equal '/'
|
24
26
|
end
|
25
27
|
|
26
|
-
it
|
28
|
+
it 'must resolve directory name to named directory' do
|
27
29
|
Complete.local_search_path('/home/').must_equal '/home'
|
28
30
|
Complete.local_search_path('/home/f').must_equal '/home'
|
29
31
|
end
|
30
32
|
|
31
|
-
it
|
33
|
+
it 'must resolve ~/ to home directory' do
|
32
34
|
Complete.local_search_path('~/').must_equal Dir.home
|
33
35
|
Complete.local_search_path('~/f').must_equal Dir.home
|
34
36
|
end
|
35
37
|
|
36
|
-
it
|
38
|
+
it 'must resolve ./ to working directory' do
|
37
39
|
Complete.local_search_path('./').must_equal Dir.pwd
|
38
40
|
Complete.local_search_path('./f').must_equal Dir.pwd
|
39
41
|
end
|
40
42
|
|
41
|
-
it
|
43
|
+
it 'must resolve ../ to parent directory' do
|
42
44
|
Complete.local_search_path('../').must_equal File.dirname(Dir.pwd)
|
43
45
|
Complete.local_search_path('../f').must_equal File.dirname(Dir.pwd)
|
44
46
|
end
|
45
47
|
|
46
|
-
it
|
47
|
-
Complete.local_search_path('~bogus')
|
48
|
+
it 'must resolve a bogus string to working directory' do
|
49
|
+
Complete.local_search_path('~bogus/bogus').must_equal Dir.pwd
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
|
-
describe
|
53
|
+
describe 'when finding potential local tab completions' do
|
52
54
|
def check(path)
|
53
|
-
100.times.all? do
|
55
|
+
100.times.all? do
|
54
56
|
prefix = path + random_string(5)
|
55
57
|
Complete.local(prefix).all? { |match| match.start_with?(prefix) }
|
56
58
|
end.must_equal true
|
57
|
-
1000.times.any? do
|
59
|
+
1000.times.any? do
|
58
60
|
prefix = path + random_string(5)
|
59
61
|
!Complete.local(prefix).empty?
|
60
62
|
end.must_equal true
|
61
63
|
end
|
62
64
|
|
63
|
-
it
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
it
|
68
|
-
|
65
|
+
it 'seed must prefix results for unqualified string' do
|
66
|
+
check('')
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'seed must prefix results for /' do
|
70
|
+
check('/')
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'seed must prefix results for named directory' do
|
74
|
+
check('/home/')
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'seed must prefix results for ~/' do
|
78
|
+
check('~/')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'seed must prefix results for ./' do
|
82
|
+
check('./')
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'seed must prefix results for ../' do
|
86
|
+
check('../')
|
87
|
+
end
|
69
88
|
|
70
89
|
it "won't raise an exception on a bogus string" do
|
71
90
|
Complete.local('~bogus')
|
72
91
|
end
|
73
92
|
end
|
74
93
|
|
75
|
-
describe
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
94
|
+
describe 'when finding local directory tab completions' do
|
95
|
+
it 'must include all directories and only directories' do
|
96
|
+
entries = Dir.entries(Dir.pwd).select do |entry|
|
97
|
+
File.directory?(entry) && !/^..?$/.match(entry)
|
98
|
+
end
|
99
|
+
matches = Complete.local_dir('').map { |match| match.chomp('/') }
|
100
|
+
matches.sort.must_equal entries.sort
|
80
101
|
end
|
102
|
+
|
103
|
+
it 'must append a / to the end of options' do
|
104
|
+
Complete.local_dir('').all? { |option| option.end_with?('/') }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'when resolving a remote search path' do
|
109
|
+
client = DropboxClient.new(Settings[:access_token])
|
110
|
+
TestUtils.ignore(DropboxError) { client.file_create_folder('/testing') }
|
81
111
|
state = State.new(client)
|
82
112
|
state.pwd = '/testing'
|
83
113
|
|
84
|
-
it
|
114
|
+
it 'must resolve unqualified string to working directory' do
|
85
115
|
Complete.remote_search_path('', state).must_equal state.pwd
|
86
116
|
Complete.remote_search_path('f', state).must_equal state.pwd
|
87
117
|
end
|
88
118
|
|
89
|
-
it
|
119
|
+
it 'must resolve / to root directory' do
|
90
120
|
Complete.remote_search_path('/', state).must_equal '/'
|
91
121
|
Complete.remote_search_path('/f', state).must_equal '/'
|
92
122
|
end
|
93
123
|
|
94
|
-
it
|
124
|
+
it 'must resolve directory name to named directory' do
|
95
125
|
Complete.remote_search_path('/testing/', state).must_equal '/testing'
|
96
126
|
Complete.remote_search_path('/testing/f', state).must_equal '/testing'
|
97
127
|
end
|
98
128
|
|
99
|
-
it
|
129
|
+
it 'must resolve ./ to working directory' do
|
100
130
|
Complete.remote_search_path('./', state).must_equal state.pwd
|
101
131
|
Complete.remote_search_path('./f', state).must_equal state.pwd
|
102
132
|
end
|
103
133
|
|
104
|
-
it
|
134
|
+
it 'must resolve ../ to parent directory' do
|
105
135
|
parent = File.dirname(state.pwd)
|
106
136
|
Complete.remote_search_path('../', state).must_equal parent
|
107
137
|
Complete.remote_search_path('../f', state).must_equal parent
|
108
138
|
end
|
109
139
|
end
|
140
|
+
|
141
|
+
describe 'when finding remote tab completions' do
|
142
|
+
client = DropboxClient.new(Settings[:access_token])
|
143
|
+
state = State.new(client)
|
144
|
+
state.pwd = '/testing'
|
145
|
+
Commands::RM.exec(client, state, '/testing/*')
|
146
|
+
%w(/testing /testing/one /testing/two).each do |dir|
|
147
|
+
Commands::MKDIR.exec(client, state, dir) unless state.metadata(dir)
|
148
|
+
end
|
149
|
+
`echo hello > test.txt`
|
150
|
+
Commands::PUT.exec(client, state, 'test.txt')
|
151
|
+
`rm test.txt`
|
152
|
+
|
153
|
+
it 'must return only matches of which the string is a prefix' do
|
154
|
+
Complete.remote('t', state).must_equal ['two/', 'test.txt ']
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'must return only directories if requested' do
|
158
|
+
Complete.remote_dir('', state).must_equal %w(one/ two/)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe 'when resolving command names' do
|
163
|
+
before do
|
164
|
+
@words = %w(plank plague plonk lake lag lock)
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'must return matches if and only if the string is a prefix' do
|
168
|
+
Complete.command('pla', @words).length.must_equal 2
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'must return matches that end with a space' do
|
172
|
+
Complete.command('plank', @words).must_equal ['plank ']
|
173
|
+
end
|
174
|
+
end
|
110
175
|
end
|
data/spec/settings_spec.rb
CHANGED
@@ -1,14 +1,23 @@
|
|
1
|
+
require 'fileutils'
|
1
2
|
require 'minitest/autorun'
|
2
3
|
|
3
|
-
|
4
|
+
def suppress_warnings
|
5
|
+
prev_verbose, $VERBOSE = $VERBOSE, nil
|
6
|
+
yield
|
7
|
+
$VERBOSE = prev_verbose
|
8
|
+
end
|
9
|
+
|
10
|
+
suppress_warnings { require_relative '../lib/droxi/settings' }
|
4
11
|
|
5
12
|
describe Settings do
|
6
13
|
KEY, VALUE = :test_key, :test_value
|
14
|
+
RC_PATH = File.expand_path('~/.config/droxi/testrc')
|
15
|
+
Settings.config_file_path = RC_PATH
|
7
16
|
|
8
17
|
describe 'when attempting access with a bogus key' do
|
9
18
|
it 'must return nil' do
|
10
19
|
Settings.delete(KEY)
|
11
|
-
Settings[KEY].
|
20
|
+
Settings[KEY].must_be_nil
|
12
21
|
end
|
13
22
|
end
|
14
23
|
|
@@ -16,6 +25,7 @@ describe Settings do
|
|
16
25
|
it 'must return the associated value' do
|
17
26
|
Settings[KEY] = VALUE
|
18
27
|
Settings[KEY].must_equal VALUE
|
28
|
+
Settings.delete(KEY)
|
19
29
|
end
|
20
30
|
end
|
21
31
|
|
@@ -30,7 +40,7 @@ describe Settings do
|
|
30
40
|
describe 'when deleting a bogus key' do
|
31
41
|
it 'must return nil' do
|
32
42
|
Settings.delete(KEY)
|
33
|
-
Settings.delete(KEY).
|
43
|
+
Settings.delete(KEY).must_be_nil
|
34
44
|
end
|
35
45
|
end
|
36
46
|
|
@@ -38,7 +48,7 @@ describe Settings do
|
|
38
48
|
it 'must delete the key and return the associated value' do
|
39
49
|
Settings[KEY] = VALUE
|
40
50
|
Settings.delete(KEY).must_equal VALUE
|
41
|
-
Settings[KEY].
|
51
|
+
Settings[KEY].must_be_nil
|
42
52
|
end
|
43
53
|
end
|
44
54
|
|
@@ -46,6 +56,7 @@ describe Settings do
|
|
46
56
|
it 'must return true for valid keys' do
|
47
57
|
Settings[KEY] = VALUE
|
48
58
|
Settings.include?(KEY).must_equal true
|
59
|
+
Settings.delete(KEY)
|
49
60
|
end
|
50
61
|
|
51
62
|
it 'must return false for bogus keys' do
|
@@ -53,4 +64,26 @@ describe Settings do
|
|
53
64
|
Settings.include?(KEY).must_equal false
|
54
65
|
end
|
55
66
|
end
|
67
|
+
|
68
|
+
describe 'when reading settings from disk' do
|
69
|
+
it 'must return an empty hash when rc file is missing' do
|
70
|
+
FileUtils.rm(RC_PATH) if File.exist?(RC_PATH)
|
71
|
+
Settings.read.must_equal({})
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'must parse options correctly for valid rc file' do
|
75
|
+
IO.write(RC_PATH, "access_token=x\noldpwd=y\nbogus=z\nnonsense\n")
|
76
|
+
suppress_warnings do
|
77
|
+
Settings.read.must_equal(access_token: 'x', oldpwd: 'y')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'must restore identical settings from previous save' do
|
82
|
+
hash = { access_token: 'x', oldpwd: 'y' }
|
83
|
+
hash.each { |key, value| Settings[key] = value }
|
84
|
+
Settings.save.must_be_nil
|
85
|
+
hash.each_key { |key| Settings.delete(key) }
|
86
|
+
Settings.read.must_equal hash
|
87
|
+
end
|
88
|
+
end
|
56
89
|
end
|
data/spec/state_spec.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
require 'dropbox_sdk'
|
1
2
|
require 'minitest/autorun'
|
2
3
|
|
3
|
-
require_relative '
|
4
|
+
require_relative 'testutils'
|
5
|
+
require_relative '../lib/droxi/commands'
|
4
6
|
require_relative '../lib/droxi/settings'
|
7
|
+
require_relative '../lib/droxi/state'
|
5
8
|
|
6
9
|
describe State do
|
10
|
+
client = DropboxClient.new(Settings[:access_token])
|
11
|
+
state = State.new(client)
|
12
|
+
|
7
13
|
describe 'when initializing' do
|
8
14
|
it 'must set pwd to root' do
|
9
15
|
State.new(nil).pwd.must_equal '/'
|
@@ -18,7 +24,6 @@ describe State do
|
|
18
24
|
|
19
25
|
describe 'when setting pwd' do
|
20
26
|
it 'must change pwd and set oldpwd to previous pwd' do
|
21
|
-
state = State.new(nil)
|
22
27
|
state.pwd = '/testing'
|
23
28
|
state.pwd.must_equal '/testing'
|
24
29
|
state.pwd = '/'
|
@@ -27,8 +32,6 @@ describe State do
|
|
27
32
|
end
|
28
33
|
|
29
34
|
describe 'when resolving path' do
|
30
|
-
state = State.new(nil)
|
31
|
-
|
32
35
|
it 'must resolve root to itself' do
|
33
36
|
state.resolve_path('/').must_equal '/'
|
34
37
|
end
|
@@ -56,44 +59,69 @@ describe State do
|
|
56
59
|
|
57
60
|
describe 'when forgetting directory contents' do
|
58
61
|
before do
|
59
|
-
|
60
|
-
|
61
|
-
2.times { |i|
|
62
|
+
state.cache.add('path' => '/', 'contents' => [], 'is_dir' => true)
|
63
|
+
state.cache.add('path' => '/dir', 'contents' => [], 'is_dir' => true)
|
64
|
+
2.times { |i| state.cache.add('path' => "/dir/file#{i}") }
|
62
65
|
end
|
63
66
|
|
64
67
|
it 'must yield an error for a bogus path' do
|
65
|
-
|
66
|
-
@state.forget_contents('bogus') { |line| lines << line }
|
67
|
-
lines.length.must_equal 1
|
68
|
+
TestUtils.output_of(state, :forget_contents, 'bogus').length.must_equal 1
|
68
69
|
end
|
69
70
|
|
70
71
|
it 'must yield an error for a non-directory path' do
|
71
|
-
|
72
|
-
|
73
|
-
lines.length.must_equal 1
|
72
|
+
TestUtils.output_of(state, :forget_contents, '/dir/file0')
|
73
|
+
.length.must_equal 1
|
74
74
|
end
|
75
75
|
|
76
76
|
it 'must yield an error for an already forgotten path' do
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
lines.length.must_equal 1
|
77
|
+
state.forget_contents('/dir')
|
78
|
+
TestUtils.output_of(state, :forget_contents, '/dir')
|
79
|
+
.length.must_equal 1
|
81
80
|
end
|
82
81
|
|
83
82
|
it 'must forget contents of given directory' do
|
84
|
-
|
85
|
-
|
86
|
-
|
83
|
+
state.forget_contents('/dir')
|
84
|
+
state.cache['/dir'].include?('contents').must_equal false
|
85
|
+
state.cache.keys.any? do |key|
|
87
86
|
key.start_with?('/dir/')
|
88
87
|
end.must_equal false
|
89
88
|
end
|
90
89
|
|
91
90
|
it 'must forget contents of subdirectories' do
|
92
|
-
|
93
|
-
|
94
|
-
|
91
|
+
state.forget_contents('/')
|
92
|
+
state.cache['/'].include?('contents').must_equal false
|
93
|
+
state.cache.keys.any? do |key|
|
95
94
|
key.length > 1
|
96
95
|
end.must_equal false
|
97
96
|
end
|
98
97
|
end
|
98
|
+
|
99
|
+
describe 'when querying metadata' do
|
100
|
+
it 'must return metadata for a valid path' do
|
101
|
+
state.metadata('/testing').must_be_instance_of Hash
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'must return nil for an invalid path' do
|
105
|
+
state.metadata('/bogus').must_be_nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'when expanding patterns' do
|
110
|
+
before do
|
111
|
+
TestUtils.exact_structure(client, state, 'sub1', 'sub2')
|
112
|
+
state.pwd = '/testing'
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'must not preserve relative paths by default' do
|
116
|
+
state.expand_patterns(['*1']).must_equal ['/testing/sub1']
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'must preserve relative paths if requested' do
|
120
|
+
state.expand_patterns(['*2'], true).must_equal ['sub2']
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'must return GlobErrors for non-matches' do
|
124
|
+
state.expand_patterns(['*3']).must_equal [GlobError.new('*3')]
|
125
|
+
end
|
126
|
+
end
|
99
127
|
end
|
data/spec/testutils.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require_relative '../lib/droxi/commands'
|
2
|
+
|
3
|
+
# Module of helper methods for testing.
|
4
|
+
module TestUtils
|
5
|
+
# The remote directory under which all test-related file manipulation should
|
6
|
+
# take place.
|
7
|
+
TEST_ROOT = '/testing'
|
8
|
+
|
9
|
+
# Run the attached block, rescuing the given +Exception+ class.
|
10
|
+
def self.ignore(error_class)
|
11
|
+
yield
|
12
|
+
rescue error_class
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Call the method on the reciever with the given args and return an +Array+
|
17
|
+
# of lines of output from the method.
|
18
|
+
def self.output_of(receiver, method, *args)
|
19
|
+
lines = []
|
20
|
+
receiver.send(method, *args) { |line| lines << line }
|
21
|
+
lines
|
22
|
+
end
|
23
|
+
|
24
|
+
# Ensure that the remote directory structure under +TEST_ROOT+ contains the
|
25
|
+
# given +Array+ of paths.
|
26
|
+
def self.structure(client, state, *paths)
|
27
|
+
paths.map { |path| "#{TEST_ROOT}/#{path}" }.each do |path|
|
28
|
+
next if state.metadata(path)
|
29
|
+
if File.extname(path).empty?
|
30
|
+
Commands::MKDIR.exec(client, state, path)
|
31
|
+
else
|
32
|
+
put_temp_file(client, state, path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Ensure that the given remote paths under +TEST_ROOT+ do NOT exist.
|
38
|
+
def self.not_structure(client, state, *paths)
|
39
|
+
paths.map! { |path| "#{TEST_ROOT}/#{path}" }
|
40
|
+
dead_paths = state.contents(TEST_ROOT).select { |p| paths.include?(p) }
|
41
|
+
return if dead_paths.empty?
|
42
|
+
Commands::RM.exec(client, state, *dead_paths)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Ensure that the remote directory structure under +TEST_ROOT+ exactly
|
46
|
+
# matches the given +Array+ of paths.
|
47
|
+
def self.exact_structure(client, state, *paths)
|
48
|
+
structure(client, state, *paths)
|
49
|
+
paths.map! { |path| "#{TEST_ROOT}/#{path}" }
|
50
|
+
dead_paths = state.contents(TEST_ROOT).reject { |p| paths.include?(p) }
|
51
|
+
return if dead_paths.empty?
|
52
|
+
Commands::RM.exec(client, state, *dead_paths)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Creates a remote file at the given path.
|
58
|
+
def self.put_temp_file(client, state, path)
|
59
|
+
`mkdir testing`
|
60
|
+
basename = File.basename(path)
|
61
|
+
`touch testing/#{basename}`
|
62
|
+
Commands::PUT.exec(client, state, "testing/#{basename}", path)
|
63
|
+
`rm -rf testing`
|
64
|
+
end
|
65
|
+
end
|