droxi 0.0.5 → 0.1.0
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.
- 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
|