ollama-ruby 0.10.0 → 0.12.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/CHANGES.md +58 -1
- data/README.md +3 -2
- data/Rakefile +6 -5
- data/bin/ollama_chat +82 -35
- data/lib/ollama/client.rb +5 -5
- data/lib/ollama/documents/cache/common.rb +5 -1
- data/lib/ollama/documents/cache/memory_cache.rb +1 -1
- data/lib/ollama/documents/cache/records.rb +87 -0
- data/lib/ollama/documents/cache/redis_backed_memory_cache.rb +2 -1
- data/lib/ollama/documents/cache/redis_cache.rb +3 -10
- data/lib/ollama/documents/cache/sqlite_cache.rb +215 -0
- data/lib/ollama/documents/splitters/semantic.rb +1 -0
- data/lib/ollama/documents.rb +35 -62
- data/lib/ollama/handlers/say.rb +42 -3
- data/lib/ollama/utils/chooser.rb +15 -1
- data/lib/ollama/utils/tags.rb +2 -1
- data/lib/ollama/version.rb +1 -1
- data/ollama-ruby.gemspec +12 -11
- data/spec/ollama/documents/{memory_cache_spec.rb → cache/memory_cache_spec.rb} +37 -3
- data/spec/ollama/documents/{redis_backed_memory_cache_spec.rb → cache/redis_backed_memory_cache_spec.rb} +19 -7
- data/spec/ollama/documents/{redis_cache_spec.rb → cache/redis_cache_spec.rb} +34 -19
- data/spec/ollama/documents/cache/sqlite_cache_spec.rb +141 -0
- data/spec/ollama/handlers/say_spec.rb +73 -10
- data/spec/ollama/utils/fetcher_spec.rb +24 -24
- data/spec/ollama/utils/tags_spec.rb +7 -2
- metadata +72 -40
@@ -1,17 +1,21 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe Ollama::Documents::RedisBackedMemoryCache do
|
4
|
+
let :prefix do
|
5
|
+
'test-'
|
6
|
+
end
|
7
|
+
|
8
|
+
let :cache do
|
9
|
+
described_class.new prefix: 'test-', url: 'something'
|
10
|
+
end
|
11
|
+
|
4
12
|
it 'raises ArgumentError if url is missing' do
|
5
13
|
expect {
|
6
|
-
described_class.new prefix
|
14
|
+
described_class.new prefix:, url: nil
|
7
15
|
}.to raise_error ArgumentError
|
8
16
|
end
|
9
17
|
|
10
18
|
context 'test redis interactions' do
|
11
|
-
let :cache do
|
12
|
-
described_class.new prefix: 'test-', url: 'something'
|
13
|
-
end
|
14
|
-
|
15
19
|
let :data do
|
16
20
|
cache.instance_eval { @data }
|
17
21
|
end
|
@@ -31,12 +35,10 @@ RSpec.describe Ollama::Documents::RedisBackedMemoryCache do
|
|
31
35
|
end
|
32
36
|
|
33
37
|
it 'can be instantiated and initialized' do
|
34
|
-
cache = described_class.new prefix: 'test-', url: 'something'
|
35
38
|
expect(cache).to be_a described_class
|
36
39
|
end
|
37
40
|
|
38
41
|
it 'defaults to nil object_class' do
|
39
|
-
cache = described_class.new prefix: 'test-', url: 'something'
|
40
42
|
expect(cache.object_class).to be_nil
|
41
43
|
end
|
42
44
|
|
@@ -77,6 +79,16 @@ RSpec.describe Ollama::Documents::RedisBackedMemoryCache do
|
|
77
79
|
cache.delete(key)
|
78
80
|
end
|
79
81
|
|
82
|
+
it 'can iterate over keys, values' do
|
83
|
+
key, value = 'foo', { 'test' => true }
|
84
|
+
expect(redis).to receive(:set).with('test-' + key, JSON(value))
|
85
|
+
cache[key] = value
|
86
|
+
cache.each do |k, v|
|
87
|
+
expect(k).to eq prefix + key
|
88
|
+
expect(v).to eq value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
80
92
|
it 'returns size' do
|
81
93
|
expect(cache).to receive(:count).and_return 3
|
82
94
|
expect(cache.size).to eq 3
|
@@ -1,33 +1,35 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe Ollama::Documents::RedisCache do
|
4
|
+
let :prefix do
|
5
|
+
'test-'
|
6
|
+
end
|
7
|
+
|
8
|
+
let :cache do
|
9
|
+
described_class.new prefix:, url: 'something'
|
10
|
+
end
|
11
|
+
|
4
12
|
it 'can be instantiated' do
|
5
|
-
cache = described_class.new prefix: 'test-', url: 'something'
|
6
13
|
expect(cache).to be_a described_class
|
7
14
|
end
|
8
15
|
|
9
16
|
it 'defaults to nil object_class' do
|
10
|
-
cache = described_class.new prefix: 'test-', url: 'something'
|
11
17
|
expect(cache.object_class).to be_nil
|
12
18
|
end
|
13
19
|
|
14
20
|
it 'can be configured with object_class' do
|
15
21
|
object_class = Class.new(JSON::GenericObject)
|
16
|
-
cache = described_class.new(prefix
|
22
|
+
cache = described_class.new(prefix:, url: 'something', object_class:)
|
17
23
|
expect(cache.object_class).to eq object_class
|
18
24
|
end
|
19
25
|
|
20
26
|
it 'raises ArgumentError if url is missing' do
|
21
27
|
expect {
|
22
|
-
described_class.new prefix
|
28
|
+
described_class.new prefix:, url: nil
|
23
29
|
}.to raise_error ArgumentError
|
24
30
|
end
|
25
31
|
|
26
32
|
context 'test redis interactions' do
|
27
|
-
let :cache do
|
28
|
-
described_class.new prefix: 'test-', url: 'something'
|
29
|
-
end
|
30
|
-
|
31
33
|
let :redis do
|
32
34
|
double('Redis')
|
33
35
|
end
|
@@ -42,43 +44,56 @@ RSpec.describe Ollama::Documents::RedisCache do
|
|
42
44
|
|
43
45
|
it 'can get a key' do
|
44
46
|
key = 'foo'
|
45
|
-
expect(redis).to receive(:get).with(
|
47
|
+
expect(redis).to receive(:get).with(prefix + key).and_return '"some_json"'
|
46
48
|
expect(cache[key]).to eq 'some_json'
|
47
49
|
end
|
48
50
|
|
49
51
|
it 'can set a value for a key' do
|
50
52
|
key, value = 'foo', { test: true }
|
51
|
-
expect(redis).to receive(:set).with(
|
53
|
+
expect(redis).to receive(:set).with(prefix + key, JSON(value), ex: nil)
|
52
54
|
cache[key] = value
|
53
55
|
end
|
54
56
|
|
55
57
|
it 'can set a value for a key with ttl' do
|
56
|
-
cache = described_class.new prefix
|
58
|
+
cache = described_class.new prefix:, url: 'something', ex: 3_600
|
57
59
|
key, value = 'foo', { test: true }
|
58
|
-
expect(redis).to receive(:set).with(
|
60
|
+
expect(redis).to receive(:set).with(prefix + key, JSON(value), ex: 3_600)
|
59
61
|
cache[key] = value
|
60
|
-
expect(redis).to receive(:ttl).with(
|
62
|
+
expect(redis).to receive(:ttl).with(prefix + key).and_return 3_600
|
61
63
|
expect(cache.ttl(key)).to eq 3_600
|
62
64
|
end
|
63
65
|
|
64
66
|
it 'can determine if key exists' do
|
65
67
|
key = 'foo'
|
66
|
-
expect(redis).to receive(:exists?).with(
|
68
|
+
expect(redis).to receive(:exists?).with(prefix + key).and_return(false, true)
|
67
69
|
expect(cache.key?('foo')).to eq false
|
68
70
|
expect(cache.key?('foo')).to eq true
|
69
71
|
end
|
70
72
|
|
71
73
|
it 'can delete' do
|
72
74
|
key = 'foo'
|
73
|
-
expect(redis).to receive(:del).with(
|
75
|
+
expect(redis).to receive(:del).with(prefix + key)
|
74
76
|
cache.delete(key)
|
75
77
|
end
|
76
78
|
|
79
|
+
it 'can iterate over keys, values' do
|
80
|
+
key, value = 'foo', { 'test' => true }
|
81
|
+
expect(redis).to receive(:set).with(prefix + key, JSON(value), ex: nil)
|
82
|
+
cache[key] = value
|
83
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
84
|
+
and_yield("#{prefix}foo")
|
85
|
+
expect(redis).to receive(:get).with(prefix + key).and_return(JSON(test: true))
|
86
|
+
cache.each do |k, v|
|
87
|
+
expect(k).to eq prefix + key
|
88
|
+
expect(v).to eq value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
77
92
|
it 'returns size' do
|
78
|
-
|
79
|
-
and_yield(
|
80
|
-
and_yield(
|
81
|
-
and_yield(
|
93
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
94
|
+
and_yield("#{prefix}foo").
|
95
|
+
and_yield("#{prefix}bar").
|
96
|
+
and_yield("#{prefix}baz")
|
82
97
|
expect(cache.size).to eq 3
|
83
98
|
end
|
84
99
|
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Ollama::Documents::SQLiteCache do
|
4
|
+
let :prefix do
|
5
|
+
'test-'
|
6
|
+
end
|
7
|
+
|
8
|
+
let :test_value do
|
9
|
+
{
|
10
|
+
key: 'test',
|
11
|
+
text: 'test text',
|
12
|
+
norm: 0.5,
|
13
|
+
source: 'for-test.txt',
|
14
|
+
tags: %w[ test ],
|
15
|
+
embedding: [ 0.5 ] * 1_024,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
let :cache do
|
20
|
+
described_class.new prefix:
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can be instantiated' do
|
24
|
+
expect(cache).to be_a described_class
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'defaults to :memory: mode' do
|
28
|
+
expect(cache.filename).to eq ':memory:'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'can be switchted to file mode' do
|
32
|
+
expect(SQLite3::Database).to receive(:new).with('foo.sqlite').
|
33
|
+
and_return(double.as_null_object)
|
34
|
+
cache = described_class.new prefix:, filename: 'foo.sqlite'
|
35
|
+
expect(cache.filename).to eq 'foo.sqlite'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'can get/set a key' do
|
39
|
+
key, value = 'foo', test_value
|
40
|
+
queried_value = nil
|
41
|
+
expect {
|
42
|
+
cache[key] = value
|
43
|
+
}.to change {
|
44
|
+
queried_value = cache[key]
|
45
|
+
}.from(nil).to(Ollama::Documents::Record[value])
|
46
|
+
expect(queried_value.embedding).to eq [ 0.5 ] * 1_024
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'can determine if key exists' do
|
50
|
+
key, value = 'foo', test_value
|
51
|
+
expect {
|
52
|
+
cache[key] = value
|
53
|
+
}.to change {
|
54
|
+
cache.key?(key)
|
55
|
+
}.from(false).to(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can set key with different prefixes' do
|
59
|
+
key, value = 'foo', test_value
|
60
|
+
expect {
|
61
|
+
cache[key] = value
|
62
|
+
}.to change {
|
63
|
+
cache.size
|
64
|
+
}.from(0).to(1)
|
65
|
+
cache2 = cache.dup
|
66
|
+
cache2.prefix = 'test2-'
|
67
|
+
expect {
|
68
|
+
cache2[key] = value
|
69
|
+
}.to change {
|
70
|
+
cache2.size
|
71
|
+
}.from(0).to(1)
|
72
|
+
expect(cache.size).to eq 1
|
73
|
+
s = 0
|
74
|
+
cache.full_each { s += 1 }
|
75
|
+
expect(s).to eq 2
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'can delete' do
|
79
|
+
key, value = 'foo', test_value
|
80
|
+
expect(cache.delete(key)).to be_falsy
|
81
|
+
cache[key] = value
|
82
|
+
expect {
|
83
|
+
expect(cache.delete(key)).to be_truthy
|
84
|
+
}.to change {
|
85
|
+
cache.key?(key)
|
86
|
+
}.from(true).to(false)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'returns size' do
|
90
|
+
key, value = 'foo', test_value
|
91
|
+
expect {
|
92
|
+
cache[key] = value
|
93
|
+
}.to change {
|
94
|
+
cache.size
|
95
|
+
}.from(0).to(1)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'can convert_to_vector' do
|
99
|
+
vector = [ 23.0, 666.0 ]
|
100
|
+
expect(cache.convert_to_vector(vector)).to eq vector
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'can clear' do
|
104
|
+
key, value = 'foo', { embedding: [ 0.5 ] * 1_024 }
|
105
|
+
cache[key] = value
|
106
|
+
expect {
|
107
|
+
expect(cache.clear).to eq cache
|
108
|
+
}.to change {
|
109
|
+
cache.size
|
110
|
+
}.from(1).to(0)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'can clear for tags' do
|
114
|
+
key, value = 'foo', { tags: %w[ foo ], embedding: [ 0.5 ] * 1_024 }
|
115
|
+
cache[key] = value
|
116
|
+
key, value = 'bar', { embedding: [ 0.5 ] * 1_024 }
|
117
|
+
cache[key] = value
|
118
|
+
expect {
|
119
|
+
expect(cache.clear_for_tags(%w[ #foo ])).to eq cache
|
120
|
+
}.to change {
|
121
|
+
cache.size
|
122
|
+
}.from(2).to(1)
|
123
|
+
expect(cache).not_to be_key 'foo'
|
124
|
+
expect(cache).to be_key 'bar'
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'can return tags' do
|
128
|
+
key, value = 'foo', { tags: %w[ foo ], embedding: [ 0.5 ] * 1_024 }
|
129
|
+
cache[key] = value
|
130
|
+
key, value = 'bar', { tags: %w[ bar baz ], embedding: [ 0.5 ] * 1_024 }
|
131
|
+
cache[key] = value
|
132
|
+
tags = cache.tags
|
133
|
+
expect(tags).to be_a Ollama::Utils::Tags
|
134
|
+
expect(tags.to_a).to eq %w[ bar baz foo ]
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'can iterate over keys under a prefix' do
|
138
|
+
cache['foo'] = test_value
|
139
|
+
expect(cache.to_a).to eq [ [ 'test-foo', Ollama::Documents::Record[test_value] ] ]
|
140
|
+
end
|
141
|
+
end
|
@@ -1,30 +1,93 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe Ollama::Handlers::Say do
|
4
|
+
let :say do
|
5
|
+
described_class.new
|
6
|
+
end
|
7
|
+
|
4
8
|
it 'has .to_proc' do
|
5
9
|
expect_any_instance_of(described_class).to receive(:call).with(:foo)
|
6
10
|
described_class.call(:foo)
|
7
11
|
end
|
8
12
|
|
9
|
-
it 'can
|
10
|
-
|
13
|
+
it 'can be instantiated' do
|
14
|
+
expect(say).to be_a described_class
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can be instantiated with a given voice' do
|
18
|
+
expect_any_instance_of(described_class).to receive(:command).
|
19
|
+
with(hash_including(voice: 'TheVoice')).and_return %w[ true ]
|
20
|
+
say = described_class.new(voice: 'TheVoice')
|
21
|
+
expect(say).to be_a described_class
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'command' do
|
25
|
+
it 'can be instantiated interactively' do
|
26
|
+
expect_any_instance_of(described_class).to receive(:command).
|
27
|
+
with(hash_including(interactive: true)).and_return %w[ true ]
|
28
|
+
say = described_class.new(interactive: true)
|
29
|
+
expect(say).to be_a described_class
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'can set the voice' do
|
33
|
+
expect(say.send(:command, voice: 'TheVoice', interactive: nil)).to eq(
|
34
|
+
%w[ say -v TheVoice ]
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'can be instantiated interactively with green' do
|
39
|
+
expect_any_instance_of(described_class).to receive(:command).
|
40
|
+
with(hash_including(interactive: 'green')).and_return %w[ true ]
|
41
|
+
say = described_class.new(interactive: 'green')
|
42
|
+
expect(say).to be_a described_class
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'can set interactive mode' do
|
46
|
+
expect(say.send(:command, voice: nil, interactive: true)).to eq(
|
47
|
+
%w[ say -i ]
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'can set interactive mode to green' do
|
52
|
+
expect(say.send(:command, voice: nil, interactive: 'green')).to eq(
|
53
|
+
%w[ say --interactive=green ]
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can say response' do
|
59
|
+
output = double('output', :sync= => true, closed?: false)
|
11
60
|
expect(output).to receive(:print).with('testing')
|
12
61
|
expect(output).to receive(:close)
|
13
|
-
|
62
|
+
say = described_class.new(output:)
|
14
63
|
response = double('response', response: 'testing', done: false)
|
15
|
-
|
64
|
+
say.call(response)
|
16
65
|
response = double('response', response: nil, message: nil, done: true)
|
17
|
-
|
66
|
+
say.call(response)
|
18
67
|
end
|
19
68
|
|
20
|
-
it 'can
|
21
|
-
output = double('output', :sync= => true)
|
69
|
+
it 'can say message content' do
|
70
|
+
output = double('output', :sync= => true, closed?: false)
|
22
71
|
expect(output).to receive(:print).with('testing')
|
23
72
|
expect(output).to receive(:close)
|
24
|
-
|
73
|
+
say = described_class.new(output:)
|
25
74
|
response = double('response', response: nil, message: double(content: 'testing'), done: false)
|
26
|
-
|
75
|
+
say.call(response)
|
76
|
+
response = double('response', response: nil, message: nil, done: true)
|
77
|
+
say.call(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'can reopen output if closed' do
|
81
|
+
output = double('output', :sync= => true, closed?: true)
|
82
|
+
reopened_output = double('output', :sync= => true, closed?: false, pid: 666)
|
83
|
+
expect(reopened_output).to receive(:print).with('testing')
|
84
|
+
expect(reopened_output).to receive(:close)
|
85
|
+
say = described_class.new(output:)
|
86
|
+
expect(say).to receive(:open_output).and_return(reopened_output)
|
87
|
+
response = double('response', response: 'testing', done: false)
|
88
|
+
say.call(response)
|
27
89
|
response = double('response', response: nil, message: nil, done: true)
|
28
|
-
|
90
|
+
say.call(response)
|
29
91
|
end
|
92
|
+
|
30
93
|
end
|
@@ -77,12 +77,12 @@ RSpec.describe Ollama::Utils::Fetcher do
|
|
77
77
|
stub_request(:get, url).
|
78
78
|
with(headers: fetcher.headers).
|
79
79
|
to_return(status: 500)
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
80
|
+
expect(STDERR).to receive(:puts).with(/cannot.*get.*#{url}/i)
|
81
|
+
fetcher.get(url) do |tmp|
|
82
|
+
expect(tmp).to be_a StringIO
|
83
|
+
expect(tmp.read).to eq ''
|
84
|
+
expect(tmp.content_type).to eq 'text/plain'
|
85
|
+
end
|
86
86
|
end
|
87
87
|
|
88
88
|
it 'can redirect' do
|
@@ -107,7 +107,7 @@ RSpec.describe Ollama::Utils::Fetcher do
|
|
107
107
|
|
108
108
|
it 'can .execute and fail' do
|
109
109
|
expect(IO).to receive(:popen).and_raise StandardError
|
110
|
-
|
110
|
+
expect(STDERR).to receive(:puts).with(/cannot.*execute.*foobar/i)
|
111
111
|
described_class.execute('foobar') do |file|
|
112
112
|
expect(file).to be_a StringIO
|
113
113
|
expect(file.read).to be_empty
|
@@ -115,23 +115,23 @@ RSpec.describe Ollama::Utils::Fetcher do
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
118
|
+
describe '.normalize_url' do
|
119
|
+
it 'can handle umlauts' do
|
120
|
+
expect(described_class.normalize_url('https://foo.de/bär')).to eq(
|
121
|
+
'https://foo.de/b%C3%A4r'
|
122
|
+
)
|
123
|
+
end
|
124
124
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
125
|
+
it 'can handle escaped umlauts' do
|
126
|
+
expect(described_class.normalize_url('https://foo.de/b%C3%A4r')).to eq(
|
127
|
+
'https://foo.de/b%C3%A4r'
|
128
|
+
)
|
129
|
+
end
|
130
130
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
131
|
+
it 'can remove #anchors' do
|
132
|
+
expect(described_class.normalize_url('https://foo.de#bar')).to eq(
|
133
|
+
'https://foo.de'
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
137
|
end
|
@@ -10,6 +10,11 @@ RSpec.describe Ollama::Utils::Tags do
|
|
10
10
|
expect(tags.to_a).to eq %w[ bar foo ]
|
11
11
|
end
|
12
12
|
|
13
|
+
it 'can contain unique tags with leading # characters and is sorted' do
|
14
|
+
tags = described_class.new(%w[ #bar ##foo ])
|
15
|
+
expect(tags.to_a).to eq %w[ bar foo ]
|
16
|
+
end
|
17
|
+
|
13
18
|
it 'tags can be added to it' do
|
14
19
|
tags = described_class.new([ 'foo' ])
|
15
20
|
tags.add 'bar'
|
@@ -27,13 +32,13 @@ RSpec.describe Ollama::Utils::Tags do
|
|
27
32
|
expect { tags.clear }.to change { tags.size }.from(2).to(0)
|
28
33
|
end
|
29
34
|
|
30
|
-
it 'tags can be
|
35
|
+
it 'tags can be empty' do
|
31
36
|
tags = described_class.new([ 'foo' ])
|
32
37
|
expect { tags.clear }.to change { tags.empty? }.from(false).to(true)
|
33
38
|
end
|
34
39
|
|
35
40
|
it 'can be output nicely' do
|
36
|
-
expect(described_class.new(%w[ foo bar ]).to_s).to eq '#bar #foo'
|
41
|
+
expect(described_class.new(%w[ #foo bar ]).to_s).to eq '#bar #foo'
|
37
42
|
end
|
38
43
|
|
39
44
|
it 'can be output nicely with links to source' do
|