mnogootex 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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