mnogootex 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +3 -0
- data/.rspec +0 -2
- data/.rubocop.yml +12 -0
- data/.travis.yml +20 -3
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +4 -3
- data/Guardfile +56 -0
- data/README.md +154 -16
- data/Rakefile +25 -4
- data/exe/mnogootex +2 -92
- data/lib/mnogootex/cfg.rb +62 -0
- data/lib/mnogootex/cli.rb +79 -0
- data/lib/mnogootex/core_ext.rb +11 -0
- data/lib/mnogootex/defaults.yml +6 -0
- data/lib/mnogootex/job/logger.rb +53 -0
- data/lib/mnogootex/job/porter.rb +45 -0
- data/lib/mnogootex/job/runner.rb +42 -0
- data/lib/mnogootex/job/warden.rb +99 -0
- data/lib/mnogootex/log/level.rb +17 -0
- data/lib/mnogootex/log/levels.yml +29 -0
- data/lib/mnogootex/log/line.rb +14 -0
- data/lib/mnogootex/log/matcher.rb +17 -0
- data/lib/mnogootex/log/matchers.yml +205 -0
- data/lib/mnogootex/log/processor.rb +115 -0
- data/lib/mnogootex/log.rb +23 -0
- data/lib/mnogootex/mnogoo.sh +21 -0
- data/lib/mnogootex/utils.rb +26 -0
- data/lib/mnogootex/version.rb +3 -1
- data/lib/mnogootex.rb +4 -4
- data/mnogootex.gemspec +41 -18
- data/spec/mnogootex/cfg_spec.rb +54 -0
- data/spec/mnogootex/job/porter_spec.rb +140 -0
- data/spec/mnogootex/job/runner_spec.rb +74 -0
- data/spec/mnogootex/log/processor_spec.rb +203 -0
- data/spec/mnogootex/utils_spec.rb +52 -0
- data/spec/spec_helper.rb +124 -0
- data/tty.gif +0 -0
- metadata +182 -20
- data/.gitmodules +0 -3
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/lib/mnogootex/configuration.rb +0 -46
- data/lib/mnogootex/job.rb +0 -75
@@ -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
|
data/lib/mnogootex/version.rb
CHANGED
data/lib/mnogootex.rb
CHANGED
data/mnogootex.gemspec
CHANGED
@@ -1,30 +1,53 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path(
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require
|
5
|
+
require 'mnogootex/version'
|
6
6
|
|
7
|
-
Gem::Specification.new do |spec|
|
8
|
-
spec.name =
|
7
|
+
Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
8
|
+
spec.name = 'mnogootex'
|
9
9
|
spec.version = Mnogootex::VERSION
|
10
|
-
spec.authors = [
|
11
|
-
spec.email = [
|
10
|
+
spec.authors = ['Paolo Brasolin']
|
11
|
+
spec.email = ['paolo.brasolin@gmail.com']
|
12
12
|
|
13
|
-
spec.summary =
|
14
|
-
|
15
|
-
|
16
|
-
|
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|
|
32
|
+
f.match(%r{^(test|mwe)/})
|
20
33
|
end
|
21
|
-
spec.bindir =
|
34
|
+
spec.bindir = 'exe'
|
22
35
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
-
spec.require_paths = [
|
36
|
+
spec.require_paths = ['lib']
|
24
37
|
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
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
|
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
|