mnogootex 0.2.1 → 1.0.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.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Mnogootex
6
+ module Utils
7
+ def self.short_md5(input)
8
+ [Digest::MD5.digest(input)]. # get 16 bytes of MD5
9
+ pack('m0'). # pack them into 22+2 base64 bytes (w/o trailing newline)
10
+ tr('+/', '-_'). # make then url/path-safe
11
+ chomp('==') # drop last 2 padding bytes
12
+ end
13
+
14
+ def self.humanize_bytes(size)
15
+ %w[b Kb Mb Gb Tb Pb Eb Zb Yb].reduce(size) do |magnitude, unit|
16
+ break "#{magnitude}#{unit}" if magnitude < 1024
17
+ magnitude / 1024
18
+ end
19
+ end
20
+
21
+ def self.dir_size(mask)
22
+ Dir.glob(Pathname.new(mask).join('**', '*')).
23
+ map! { |f| Pathname.new(f).size }.inject(:+) || 0
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mnogootex
2
- VERSION = "0.2.1"
4
+ VERSION = '1.0.0'
3
5
  end
data/lib/mnogootex.rb CHANGED
@@ -1,7 +1,7 @@
1
- require "mnogootex/version"
2
- require "mnogootex/configuration"
3
- require "mnogootex/job"
1
+ # frozen_string_literal: true
2
+
3
+ require 'mnogootex/version'
4
+ require 'mnogootex/cli'
4
5
 
5
6
  module Mnogootex
6
- # Your code goes here...
7
7
  end
data/mnogootex.gemspec CHANGED
@@ -1,30 +1,53 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('../lib', __FILE__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "mnogootex/version"
5
+ require 'mnogootex/version'
6
6
 
7
- Gem::Specification.new do |spec|
8
- spec.name = "mnogootex"
7
+ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
8
+ spec.name = 'mnogootex'
9
9
  spec.version = Mnogootex::VERSION
10
- spec.authors = ["Paolo Brasolin"]
11
- spec.email = ["paolo.brasolin@gmail.com"]
10
+ spec.authors = ['Paolo Brasolin']
11
+ spec.email = ['paolo.brasolin@gmail.com']
12
12
 
13
- spec.summary = %q{Mnogootex (многоꙮтех) is a device to handle the compilation of TeX sources with different preambles at one time.}
14
- spec.description = %q{Mnogootex (многоꙮтех) is a device to handle the compilation of TeX sources with different preambles at one time. This avoids wasting time when you have to submit a paper to journals using outdated or crummy document classes.}
15
- spec.homepage = "https://github.com/tetrapharmakon/mnogootex"
16
- spec.license = "MIT"
13
+ spec.summary = <<~SUMMARY.tr("\n", ' ').squeeze(' ').strip
14
+ Многоꙮтех (mnogootex) is a utility that parallelizes compilation
15
+ of a LaTeX document using different classes and offers a
16
+ meaningfully filtered output.
17
+ SUMMARY
18
+
19
+ spec.description = <<~DESCRIPTION.tr("\n", ' ').squeeze(' ').strip
20
+ Многоꙮтех (mnogootex) is a utility that parallelizes compilation
21
+ of a LaTeX document using different classes and offers a
22
+ meaningfully filtered output.
23
+ The motivating use case is maintaining a single preamble while
24
+ submitting a paper to many journals using their outdated or crummy
25
+ document classes.
26
+ DESCRIPTION
27
+
28
+ spec.homepage = 'https://github.com/tetrapharmakon/mnogootex'
29
+ spec.license = 'MIT'
17
30
 
18
31
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features|mwe|mnogootex_classes)/})
32
+ f.match(%r{^(test|mwe)/})
20
33
  end
21
- spec.bindir = "exe"
34
+ spec.bindir = 'exe'
22
35
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
36
+ spec.require_paths = ['lib']
24
37
 
25
- spec.add_development_dependency "bundler", "~> 1.16"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency 'bundler', '~> 1.16'
39
+ spec.add_development_dependency 'byebug', '~> 10.0'
40
+ spec.add_development_dependency 'dry-inflector', '~> 0.1.1'
41
+ spec.add_development_dependency 'guard', '~> 2.14'
42
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
43
+ spec.add_development_dependency 'mutant', '~> 0.8.14'
44
+ spec.add_development_dependency 'mutant-rspec', '~> 0.8.14'
45
+ spec.add_development_dependency 'rake', '~> 10.4'
46
+ spec.add_development_dependency 'rspec', '~> 3.6'
47
+ spec.add_development_dependency 'rubocop', '~> 0.52.1'
48
+ spec.add_development_dependency 'simplecov', '~> 0.15.1'
49
+ spec.add_development_dependency 'yard', '~> 0.9.12'
28
50
 
29
- spec.add_dependency "colorize"
51
+ spec.add_dependency 'colorize', '~> 0.8.1'
52
+ spec.add_dependency 'thor', '~> 0.20.0'
30
53
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'yaml'
6
+ require 'tmpdir'
7
+
8
+ require 'mnogootex/cfg'
9
+
10
+ describe Mnogootex::Cfg do
11
+ describe 'load_descending' do
12
+ context 'fake file structure' do
13
+ subject { described_class.load_descending(pathname: Pathname.new('foobar'), basename: 'cfg.yml') }
14
+
15
+ it 'raises an error on loading a fake path' do
16
+ expect { subject }.to raise_exception Errno::ENOENT
17
+ end
18
+ end
19
+
20
+ context 'real file structure' do
21
+ subject { described_class.load_descending(pathname: tmp_dir.join('A', 'B'), basename: 'cfg.yml') }
22
+ let(:defaults) { { default: true, winner: 'default' } }
23
+ let(:tmp_dir) { Pathname.new(Dir.mktmpdir).join('mnogootex-test') }
24
+
25
+ before do
26
+ tmp_dir.join('A', 'B').mkpath
27
+ tmp_dir.join('cfg.yml').write('') # NOTE: empty file
28
+ tmp_dir.join('A', 'cfg.yml').write({ parent: true, winner: 'parent', merged: { deep: true } }.to_yaml)
29
+ tmp_dir.join('A', 'B', 'cfg.yml').write({ child: true, winner: 'child', merged: {} }.to_yaml)
30
+ end
31
+
32
+ after do
33
+ tmp_dir.rmtree
34
+ end
35
+
36
+ it 'merges paths' do
37
+ expect(subject).to include(:parent)
38
+ expect(subject).to include(:child)
39
+ end
40
+
41
+ it 'merges shallowly' do
42
+ expect(subject[:merged]).to_not include(:deep)
43
+ end
44
+
45
+ it 'privileges deeper paths' do
46
+ expect(subject[:winner]).to eq('child')
47
+ end
48
+
49
+ it 'privileges non defaults' do
50
+ expect(subject[:winner]).to_not eq('default')
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'mnogootex/job/porter'
5
+
6
+ require 'mnogootex/utils'
7
+
8
+ describe Mnogootex::Job::Porter do
9
+ # NOTE: using mktmpdir instead of tmpdir allows parallelization
10
+ let(:test_dir) { Pathname.new(Dir.mktmpdir).join('mnogootex-test') }
11
+ before { test_dir.mkpath }
12
+ after { test_dir.rmtree }
13
+
14
+ describe '.new' do
15
+ it 'requires a source' do
16
+ expect { described_class.new hid: '1', source_path: nil }.to raise_exception(TypeError)
17
+ end
18
+
19
+ it 'requires source to exist' do
20
+ expect { described_class.new hid: '1', source_path: 'foobar' }.to raise_exception(Errno::ENOENT)
21
+ end
22
+
23
+ it 'accepts string as source' do
24
+ expect { described_class.new hid: '1', source_path: test_dir.to_s }.to_not raise_exception
25
+ end
26
+
27
+ it 'accepts pathname as source' do
28
+ expect { described_class.new hid: '1', source_path: test_dir }.to_not raise_exception
29
+ end
30
+ end
31
+
32
+ describe '#target_dir' do
33
+ before do
34
+ test_dir.join('a').mkpath
35
+ test_dir.join('b').mkpath
36
+ test_dir.join('a', 'main.file').write('')
37
+ test_dir.join('b', 'main.file').write('')
38
+ end
39
+ let(:porter_a1) { described_class.new hid: '1', source_path: test_dir.join('a', 'main.file') }
40
+ let(:porter_a2) { described_class.new hid: '2', source_path: test_dir.join('a', 'main.file') }
41
+ let(:porter_b1) { described_class.new hid: '1', source_path: test_dir.join('b', 'main.file') }
42
+
43
+ it 'has a deterministic location' do
44
+ hash = Mnogootex::Utils.short_md5(test_dir.join('a', 'main.file').realpath.to_s)
45
+ expect(porter_a1.target_dir.to_s).
46
+ to match(%r{\A.+/mnogootex/#{hash}/#{1}\z})
47
+ end
48
+
49
+ it 'discriminates by hid' do
50
+ expect(porter_a1.target_dir).to_not eq(porter_a2.target_dir)
51
+ end
52
+
53
+ it 'discriminates by source' do
54
+ expect(porter_a1.target_dir).to_not eq(porter_b1.target_dir)
55
+ end
56
+ end
57
+
58
+ describe '#target_path' do
59
+ let(:source_path) { test_dir.join('a', 'main.file') }
60
+ let(:porter) { described_class.new hid: '1', source_path: source_path }
61
+
62
+ before do
63
+ source_path.dirname.mkpath
64
+ source_path.write('')
65
+ end
66
+
67
+ it 'rescopes source basename into target dirname' do
68
+ expect(porter.target_path.basename).to eq(source_path.basename)
69
+ expect(porter.target_path.dirname).to eq(porter.target_dir)
70
+ end
71
+ end
72
+
73
+ describe '#provide' do
74
+ let(:source_path) { test_dir.join('A', 'main.file') }
75
+
76
+ before do
77
+ test_dir.join('A', 'B').mkpath
78
+ test_dir.join('A', 'main.file').write('')
79
+ test_dir.join('A', '.mnogootex.yml').write('')
80
+ test_dir.join('A', '.dotfile').write('')
81
+ test_dir.join('A', 'B', 'ancillary.file').write('')
82
+ end
83
+
84
+ subject { described_class.new hid: 'job_id', source_path: source_path }
85
+
86
+ it 'creates target directory' do
87
+ subject.provide
88
+ expect(subject.target_dir).to be_directory
89
+ end
90
+
91
+ it 'ignores configuration file' do
92
+ test_dir.join('A', '.mnogootex.yml').unlink
93
+ subject.provide
94
+ expect(subject.target_dir.join('.mnogootex.yml')).to_not exist
95
+ end
96
+
97
+ it 'creates link to source' do
98
+ subject.provide
99
+ expect(subject.target_dir.join('.mnogootex.src').readlink).to eq(source_path.realpath)
100
+ end
101
+
102
+ def relative_subtree(pathname)
103
+ Pathname.glob(pathname.join('**', '{.*,*}')).map do |child|
104
+ child.relative_path_from(pathname)
105
+ end
106
+ end
107
+
108
+ it 'copies all source files' do
109
+ subject.provide
110
+ subject.target_dir.join('.mnogootex.src').unlink
111
+ source_path.dirname.join('.mnogootex.yml').unlink
112
+ # NOTE: unlinking so comparison is easier to write
113
+ expect(relative_subtree(source_path.dirname)).
114
+ to eq(relative_subtree(subject.target_dir))
115
+ end
116
+ end
117
+
118
+ describe '#clobber' do
119
+ let(:source_path) { test_dir.join('A', 'main.file') }
120
+
121
+ before do
122
+ test_dir.join('A', 'B').mkpath
123
+ test_dir.join('A', 'main.file').write('')
124
+ end
125
+
126
+ subject { described_class.new hid: 'job_id', source_path: source_path }
127
+
128
+ it 'cleans up target dir if it exists' do
129
+ subject.provide
130
+ expect(subject.target_dir).to exist
131
+ subject.clobber
132
+ expect(subject.target_dir).to_not exist
133
+ end
134
+
135
+ it 'does nothing if target dir does not exist' do
136
+ expect(subject.target_dir).to_not exist
137
+ expect { subject.clobber }.to_not raise_exception
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'mnogootex/job/runner'
5
+
6
+ require 'tmpdir'
7
+
8
+ describe Mnogootex::Job::Runner do
9
+ let(:test_dir) { Pathname.new(Dir.mktmpdir) }
10
+ before { test_dir.mkpath }
11
+ after { test_dir.rmtree }
12
+
13
+ it 'executes commandline in given dir' do
14
+ runner = described_class.new(cl: 'pwd', chdir: test_dir)
15
+ expect(runner).to be_successful
16
+ expect(runner.log_lines.join.chomp).to eq(test_dir.realpath.to_s)
17
+ end
18
+
19
+ describe '#alive?' do
20
+ it 'is true if thread is running' do
21
+ runner = described_class.new(cl: 'sleep 0.05', chdir: test_dir)
22
+ expect(runner).to be_alive
23
+ end
24
+
25
+ it 'is false if thread is dead' do
26
+ runner = described_class.new(cl: ':', chdir: test_dir)
27
+ sleep 0.05
28
+ expect(runner).to_not be_alive
29
+ end
30
+ end
31
+
32
+ describe '#successful?' do
33
+ it 'is true on zero exit status' do
34
+ runner = described_class.new(cl: 'exit 0', chdir: test_dir)
35
+ expect(runner).to be_successful
36
+ end
37
+
38
+ it 'is false on nonzero exit status' do
39
+ runner = described_class.new(cl: 'exit 1', chdir: test_dir)
40
+ expect(runner).to_not be_successful
41
+ end
42
+ end
43
+
44
+ describe '#count_lines' do
45
+ let!(:lns) { <<~SHELL }
46
+ lns () { i=1; while [ "$i" -le $1 ]; do echo $i; i=$(( i + 1 )); done };
47
+ SHELL
48
+
49
+ context 'dead process' do
50
+ subject { described_class.new(cl: "#{lns} lns 3", chdir: test_dir) }
51
+
52
+ before do
53
+ subject.successful? # waits on threads
54
+ end
55
+
56
+ it 'plateaus immediately at log lines count' do
57
+ [3, 3].each { |n| expect(subject.count_lines).to eq(n) }
58
+ end
59
+ end
60
+
61
+ context 'alive process' do
62
+ subject { described_class.new(cl: "#{lns} lns 3; sleep 0.20", chdir: test_dir) }
63
+
64
+ before do
65
+ subject
66
+ sleep 0.02
67
+ end
68
+
69
+ it 'unitarily increases from zero then plateaus at current line count' do
70
+ [0, 1, 2, 3, 3, 3].each { |n| expect(subject.count_lines).to eq(n) }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'yaml'
6
+
7
+ require 'mnogootex/log/processor'
8
+
9
+ describe Mnogootex::Log::Processor do
10
+ describe '.strings_to_lines!' do
11
+ it 'converts strings into lines' do
12
+ strings = "foo\nbar\n".lines
13
+
14
+ described_class.strings_to_lines! strings
15
+
16
+ expect(strings).to eq [
17
+ Mnogootex::Log::Line.new('foo'),
18
+ Mnogootex::Log::Line.new('bar')
19
+ ]
20
+ end
21
+ end
22
+
23
+ describe '.tag_lines!' do
24
+ it 'tags using short matchers' do
25
+ lines = [Mnogootex::Log::Line.new,
26
+ Mnogootex::Log::Line.new('foo'),
27
+ Mnogootex::Log::Line.new]
28
+
29
+ matchers = [Mnogootex::Log::Matcher.new(/foo/, :foo)]
30
+
31
+ described_class.tag_lines! lines, matchers: matchers
32
+
33
+ expect(lines.map(&:level)).to eq([nil, :foo, nil])
34
+ end
35
+
36
+ it 'tags using long matchers' do
37
+ lines = [Mnogootex::Log::Line.new,
38
+ Mnogootex::Log::Line.new,
39
+ Mnogootex::Log::Line.new('foo'),
40
+ Mnogootex::Log::Line.new,
41
+ Mnogootex::Log::Line.new,
42
+ Mnogootex::Log::Line.new,
43
+ Mnogootex::Log::Line.new]
44
+
45
+ matchers = [Mnogootex::Log::Matcher.new(/foo/, :foo, 3)]
46
+
47
+ described_class.tag_lines! lines, matchers: matchers
48
+
49
+ expect(lines.map(&:level)).to eq([nil, nil, :foo, :foo, :foo, nil, nil])
50
+ end
51
+
52
+ it 'tags abiding to matchers order' do
53
+ lines = [Mnogootex::Log::Line.new('foo'),
54
+ Mnogootex::Log::Line.new('something')]
55
+
56
+ matchers = [Mnogootex::Log::Matcher.new(/foo/, :winner, 1),
57
+ Mnogootex::Log::Matcher.new(/foo/, :loser, 1),
58
+ Mnogootex::Log::Matcher.new(//, :anything, 1)]
59
+
60
+ described_class.tag_lines! lines, matchers: matchers
61
+
62
+ expect(lines.map(&:level)).to eq(%i[winner anything])
63
+ end
64
+ end
65
+
66
+ describe '.filter_lines!' do
67
+ it 'raises on line with unknown level' do
68
+ lines = [Mnogootex::Log::Line.new('foo', :bar)]
69
+
70
+ levels = { foo: Mnogootex::Log::Level.new(0) }
71
+
72
+ expect do
73
+ described_class.filter_lines! lines,
74
+ levels: levels,
75
+ min_level: :foo
76
+ end.to raise_exception KeyError
77
+ end
78
+
79
+ it 'raises on unknown min level' do
80
+ lines = [Mnogootex::Log::Line.new('foo', :foo)]
81
+
82
+ levels = { foo: Mnogootex::Log::Level.new(0) }
83
+
84
+ expect do
85
+ described_class.filter_lines! lines,
86
+ levels: levels,
87
+ min_level: :bar
88
+ end.to raise_exception KeyError
89
+ end
90
+
91
+ it 'filters tagged lines by minimum level' do
92
+ lines = [Mnogootex::Log::Line.new('foo', :foo),
93
+ Mnogootex::Log::Line.new('bar', :bar),
94
+ Mnogootex::Log::Line.new('baz', :baz)]
95
+
96
+ levels = { foo: Mnogootex::Log::Level.new(0),
97
+ bar: Mnogootex::Log::Level.new(1),
98
+ baz: Mnogootex::Log::Level.new(2) }
99
+
100
+ described_class.filter_lines! lines,
101
+ levels: levels,
102
+ min_level: :bar
103
+
104
+ expect(lines.map(&:text)).to eq(%w[bar baz])
105
+ end
106
+ end
107
+
108
+ describe '.colorize_lines!' do
109
+ it 'raises on unknown min level' do
110
+ lines = [Mnogootex::Log::Line.new('foo', :foo)]
111
+
112
+ levels = { bar: Mnogootex::Log::Level.new(0) }
113
+
114
+ expect do
115
+ described_class.colorize_lines! lines, levels: levels
116
+ end.to raise_exception KeyError
117
+ end
118
+
119
+ it 'colorizes tagged lines' do
120
+ lines = [Mnogootex::Log::Line.new('foo', :foo),
121
+ Mnogootex::Log::Line.new('bar', :bar),
122
+ Mnogootex::Log::Line.new('baz', :baz)]
123
+
124
+ levels = { foo: Mnogootex::Log::Level.new(0, :foo, :red),
125
+ bar: Mnogootex::Log::Level.new(1, :bar, :green),
126
+ baz: Mnogootex::Log::Level.new(2, :baz, :blue) }
127
+
128
+ described_class.colorize_lines! lines, levels: levels
129
+
130
+ expect(lines.map(&:text)).to eq %W{\e[0;31;49mfoo\e[0m
131
+ \e[0;32;49mbar\e[0m
132
+ \e[0;34;49mbaz\e[0m}
133
+ end
134
+ end
135
+
136
+ describe '.render_lines!' do
137
+ it 'renders lines to indented terminated strings' do
138
+ lines = [Mnogootex::Log::Line.new('foo'),
139
+ Mnogootex::Log::Line.new('bar'),
140
+ Mnogootex::Log::Line.new('baz')]
141
+
142
+ described_class.render_lines! lines, indent_width: 2
143
+
144
+ expect(lines).to eq [" foo\n", " bar\n", " baz\n"]
145
+ end
146
+ end
147
+
148
+ describe '#run' do
149
+ log = <<~LOG
150
+ This is generic irrelevant information.
151
+ Hey, I'm warning you, dude. Stuff is gonna get bad.
152
+ This is also a known irrelevant information flood...
153
+ ... telling you that you'd better pay attention to warnings.
154
+ I warned you, dude. Here's an ERROR. :(
155
+ LOG
156
+
157
+ levels = { trace: Mnogootex::Log::Level.new(0, :trace),
158
+ warning: Mnogootex::Log::Level.new(1, :warning, :yellow),
159
+ error: Mnogootex::Log::Level.new(2, :error, :red) }
160
+
161
+ matchers = [Mnogootex::Log::Matcher.new(/error/i, :error, 1),
162
+ Mnogootex::Log::Matcher.new(/warning/i, :warning, 1),
163
+ Mnogootex::Log::Matcher.new(/flood/, :trace, 2),
164
+ Mnogootex::Log::Matcher.new(//, :trace, 1)]
165
+
166
+ it 'can be initilized and run' do
167
+ my_processor = described_class.new matchers: matchers,
168
+ levels: levels,
169
+ min_level: :warning,
170
+ colorize: true,
171
+ indent_width: 4
172
+
173
+ expect(my_processor.run(log.lines)).to eq(
174
+ [" \e[0;33;49mHey, I'm warning you, dude. Stuff is gonna get bad.\e[0m\n",
175
+ " \e[0;31;49mI warned you, dude. Here's an ERROR. :(\e[0m\n"]
176
+ )
177
+ end
178
+
179
+ it 'can disable colorization' do
180
+ my_processor = described_class.new matchers: matchers,
181
+ levels: levels,
182
+ min_level: :warning,
183
+ colorize: false,
184
+ indent_width: 4
185
+
186
+ expect(my_processor.run(log.lines)).to eq(
187
+ [" Hey, I'm warning you, dude. Stuff is gonna get bad.\n",
188
+ " I warned you, dude. Here's an ERROR. :(\n"]
189
+ )
190
+ end
191
+
192
+ it 'does not mutate given lines' do
193
+ my_processor = described_class.new matchers: matchers,
194
+ levels: levels,
195
+ min_level: :warning,
196
+ colorize: true,
197
+ indent_width: 4
198
+
199
+ log_lines = log.lines
200
+ expect { my_processor.run(log_lines) }.to_not(change { log_lines })
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'tmpdir'
6
+
7
+ require 'mnogootex/utils'
8
+
9
+ describe Mnogootex::Utils do
10
+ describe '.short_md5' do
11
+ it 'gives expected hash for empty string' do
12
+ expect(described_class.short_md5('')).to eq('1B2M2Y8AsgTpgAmY7PhCfg')
13
+ end
14
+
15
+ it 'gives an url/path-safe hash' do
16
+ expect(described_class.short_md5('Knuth')).to eq('KyIs0ZIec5GkG7_G-clv6Q')
17
+ end
18
+ end
19
+
20
+ describe '.humanize_bytes' do
21
+ it 'rounds to smaller unit' do
22
+ expect(described_class.humanize_bytes(1023 * 1024)).to eq('1023Kb')
23
+ expect(described_class.humanize_bytes(1024 * 1024)).to eq('1Mb')
24
+ expect(described_class.humanize_bytes(1025 * 1024)).to eq('1Mb')
25
+ end
26
+
27
+ it 'covers a reasonable scale' do
28
+ %w[b Kb Mb Gb Tb Pb Eb Zb Yb].each_with_index do |unit, index|
29
+ expect(described_class.humanize_bytes((2**10)**index)).to eq("1#{unit}")
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '.dir_size' do
35
+ let(:tmpdir) { Pathname.new(Dir.mktmpdir) }
36
+ before { tmpdir.mkpath }
37
+ after { tmpdir.rmtree }
38
+
39
+ it 'measures an empty dir' do
40
+ expect(described_class.dir_size(tmpdir)).to eq(0)
41
+ end
42
+
43
+ it 'measures a subtree' do
44
+ tmpdir.join('foo').write('foo' * 100)
45
+ tmpdir.join('bar').write('bar' * 200)
46
+ tmpdir.join('baz').mkpath
47
+ tmpdir.join('baz', 'qux').write('qux' * 300)
48
+ # NOTE: dir size is fs dependent, so let's not care about that
49
+ expect(described_class.dir_size(tmpdir.to_s)).to eq(300 + 600 + tmpdir.join('baz').size + 900)
50
+ end
51
+ end
52
+ end