helper_classes 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f2cc2efbfcfed05e72c9003554b4a7e3c344f925
4
+ data.tar.gz: 3426825c61f9d9b782dc46d507c20bca84164f43
5
+ SHA512:
6
+ metadata.gz: b19fbe9429c4097b68464cdfc05ec33bb91d9da52ebc6ae04236e88359bb33c11caa85ff9af718260df64f2f940ddf127df56ae39da1ee3adea1cbcfb8dfee45
7
+ data.tar.gz: 3d71d53497721d7e8fdeb724707d81d8363a3feaceb7db61db57aae6a28e47317214b6cec7a7bd68867062aa429d593f9b1215c465e4dc9105243e45b043e45c
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ /nbproject
3
+ .hg*
4
+ .idea
5
+ commit
6
+ .DS_Store
7
+ test/test_config
data/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ == HelperClasses
2
+
3
+ GPLv3 or any later version
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ == HelperClasses
2
+
3
+ Some simple classes to help in everyday tasks
4
+
5
+ === DPuts
6
+
7
+ Ever wanted to have detailed debug-output, but in the end even PRODUCING the
8
+ string without printing it was too long? Here comes DPuts! It let's you define
9
+ different debug-levels and the strings are only evaluated if they're about
10
+ to be printed! Great for that 10MB-dump that you only want for debugging...
11
+
12
+ Usage:
13
+
14
+ ```
15
+ include HelperClasses::DPuts
16
+
17
+ DEBUG_LVL = 5
18
+ dputs(5){ "String with some #{huge_db.inspect}" }
19
+ ```
20
+
21
+ This will evaluate and print the ```huge_db``` variable, while
22
+
23
+ ```
24
+ include HelperClasses::DPuts
25
+
26
+ DEBUG_LVL = 4
27
+ dputs(5){ "String with some #{huge_db.inspect}" }
28
+ ```
29
+
30
+ will NOT evaluate it! The debug-levels are arbitrarily chosen like this:
31
+
32
+ 0 - Errors
33
+ 1 - Info and warnings, important
34
+ 2 - Often used debug-messages - limit of production-use
35
+ 3 - More detailed debugging
36
+ 4
37
+ 5 - Dumping lots of raw data
38
+
39
+ ==== Fine-grained debugging
40
+
41
+ If you have a function with lots of _dputs_ in it, and you'd like to output
42
+ all debugging messages just from that function, you simply add
43
+
44
+ ```
45
+ dputs_func
46
+ ```
47
+
48
+ at the beginning of your function.
49
+
50
+ If you want just one specific _dputs_ to be evaluated, just change its name to
51
+ _ddputs_:
52
+
53
+ ```
54
+ DEBUG_LVL = 0
55
+
56
+ ddputs(5){"String with lots of data#{huge_var.inspect}"}
57
+ ```
58
+
59
+ will be evaluated!
60
+
61
+ === Arraysym
62
+
63
+ to_sym and to_sym! - calls .to_sym on all elements. Usage:
64
+
65
+ ```
66
+ using HelperClasses::ArraySym
67
+
68
+ p [ "a", "b" ].to_sym
69
+ ```
70
+
71
+ Produces "[ :a, :b ]"
72
+
73
+ === HashAccessor
74
+
75
+ This should be standard ruby. Access all elements of an array by
76
+ prepending a "_".
77
+
78
+ ==== Getting a value
79
+
80
+ ```
81
+ using HelperClasses::HashAccessor
82
+
83
+ p { :a => 2, "a" => 3 }._a
84
+ ```
85
+
86
+ Will print "2". So symbols have precedence over string-keys.
87
+
88
+ ==== Setting a value
89
+
90
+ ```
91
+ using HelperClasses::HashAccessor
92
+
93
+ a = { :a => 2, "a" => 3 }
94
+ a._a = 4
95
+ a._b = 5
96
+
97
+ p a
98
+ ```
99
+
100
+ Will print ```{ :a => 4, "a" => 3, :b => "5" }```
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'helper_classes'
3
+ s.version = '0.2.1'
4
+ s.date = '2015-05-12'
5
+ s.summary = 'Hash._accessor Array.to_sym and DPuts'
6
+ s.description = 'Added accessors to Hash, to_sym to Array and a nice debugging-interface called DPuts'
7
+ s.authors = ['Linus Gasser']
8
+ s.email = 'ineiti@linusetviviane.ch'
9
+
10
+ s.files = `git ls-files -z`.split("\x0")
11
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
12
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
13
+ s.require_paths = ['lib']
14
+
15
+ s.homepage =
16
+ 'https://github.com/ineiti/HelperClasses'
17
+ s.license = 'GPLv3'
18
+ end
@@ -0,0 +1,25 @@
1
+
2
+ module HelperClasses
3
+ module ArraySym
4
+ class Array
5
+ # Comptaibility for Ruby <= 2.0
6
+ if ![].respond_to? :to_h
7
+ def to_h
8
+ Hash[*self.flatten]
9
+ end
10
+ end
11
+
12
+ def to_sym
13
+ collect{|v| v.to_sym }
14
+ end
15
+
16
+ def to_sym!
17
+ self.replace( to_sym() )
18
+ end
19
+
20
+ def to_s
21
+ "[#{join(",")}]"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,111 @@
1
+ module HelperClasses
2
+ require 'thread'
3
+ require 'singleton'
4
+
5
+ module DPuts
6
+ extend self
7
+ attr_accessor :mutex, :silent, :show_time, :terminal_width, :log_file
8
+
9
+ @mutex = Mutex.new
10
+ @silent = false
11
+ @show_time = false
12
+ @terminal_width = 160
13
+ @log_file = false
14
+
15
+ def dputs_out(n, s, call)
16
+ return if DPuts.silent
17
+ if precision = DPuts.show_time
18
+ $dputs_time ||= Time.now - 120
19
+ now = Time.now
20
+ show = false
21
+ case precision
22
+ when /sec/
23
+ show = now.to_i != $dputs_time.to_i
24
+ when /min/
25
+ show = (now.to_i / 60).floor != ($dputs_time.to_i / 60).floor
26
+ when /hour/
27
+ show = (now.to_i / 3600).floor != ($dputs_time.to_i / 3600).floor
28
+ end
29
+ if show
30
+ puts "\n *** It is now: " +
31
+ Time.now.strftime('%Y-%m-%d %H:%M:%S')
32
+ $dputs_time = now
33
+ end
34
+ end
35
+ DPuts.mutex.synchronize do
36
+ width = DPuts.terminal_width.to_i || 160
37
+ width = [width - 30.0, 10].max
38
+ file, func = call.split(' ')
39
+ file = file[/^.*\/([^.]*)/, 1]
40
+ who = (':' + n.to_s + ':' + file.to_s +
41
+ func.to_s).ljust(30, ['X', 'x', '*', '-', '.', ' '][n])
42
+ lines = []
43
+ pos = 0
44
+ while (pos < s.length)
45
+ len = width
46
+ if s.length - pos > width
47
+ len = s.rindex(/[, .;=&>]/, pos + width)
48
+ len = len ? len - pos + 1 : width
49
+ if len < width / 2
50
+ len = width
51
+ end
52
+ end
53
+ lines.push s.slice(pos, len)
54
+ pos += len
55
+ end
56
+ puts who + ' ' + lines.shift.to_s
57
+ lines.each { |l|
58
+ puts ' ' * (32) + l
59
+ }
60
+ end
61
+ end
62
+
63
+
64
+ def dputs_getcaller
65
+ caller(0)[2].sub(/:.*:in/, '').sub(/block .*in /, '')
66
+ end
67
+
68
+ def dputs_func
69
+ $DPUTS_FUNCS ||= []
70
+ $DPUTS_FUNCS.push(dputs_getcaller) unless $DPUTS_FUNCS.index(dputs_getcaller)
71
+ end
72
+
73
+ def dputs_unfunc
74
+ $DPUTS_FUNCS ||= []
75
+ $DPUTS_FUNCS.index(dputs_getcaller) and $DPUTS_FUNCS.delete(dputs_getcaller)
76
+ end
77
+
78
+ def dputs(n, &s)
79
+ n *= -1 if ($DPUTS_FUNCS and $DPUTS_FUNCS.index(dputs_getcaller))
80
+ if !self.class.const_defined?(:DEBUG_LVL) or
81
+ self.class.const_get(:DEBUG_LVL) >= n
82
+ s = yield s
83
+ dputs_out(n, s, caller(0)[1])
84
+ end
85
+ end
86
+
87
+ def ddputs(n, &s)
88
+ s = yield s
89
+ #dp caller(0)
90
+ dputs_out(-n, s, caller(0)[1])
91
+ end
92
+
93
+ def dp(s)
94
+ dputs_out(0, s.class == String ? s : s.inspect, caller(0)[1])
95
+ s
96
+ end
97
+
98
+ def log_msg(mod, msg)
99
+ dputs(1) { "Info from #{mod}: #{msg}" }
100
+ return if not DPuts.log_file
101
+ File.open(DPuts.log_file, 'a') { |f|
102
+ str = Time.now.strftime("%a %y.%m.%d-%H:%M:%S #{mod}: #{msg}")
103
+ f.puts str
104
+ }
105
+ end
106
+
107
+ def dlog_msg(mod, msg)
108
+ ddputs(1){"Info from #{mod}: #{msg}"}
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,44 @@
1
+ module HelperClasses
2
+ module HashAccessor
3
+ refine Hash do
4
+ end
5
+ end
6
+ end
7
+
8
+ class Hash
9
+ # Converts all keys of a hash to syms recursively
10
+ def to_sym
11
+ ret = {}
12
+ each { |k, v|
13
+ ret[k.to_sym] = v.class == Hash ? v.to_sym : v
14
+ }
15
+ ret
16
+ end
17
+
18
+ def to_sym!
19
+ self.replace(to_sym())
20
+ end
21
+
22
+ def method_missing(s, *args)
23
+ case s.to_s
24
+ when "to_ary"
25
+ super(s, args)
26
+ when /^_.*[^=]$/
27
+ key = s.to_s.sub(/^_{1,2}/, '').to_sym
28
+ self.has_key? key and return self[key]
29
+ self.has_key? key.to_s and return self[key.to_s]
30
+ if s.to_s =~ /^__/
31
+ return self[key] = {}
32
+ else
33
+ return nil
34
+ end
35
+ when /^_.*=$/
36
+ key = /^_{1,2}(.*)=$/.match(s.to_s)[1].to_sym
37
+ self.has_key? key and return self[key] = args[0]
38
+ self.has_key? key.to_s and return self[key.to_s] = args[0]
39
+ return self[key] = args[0]
40
+ else
41
+ super(s, args)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,50 @@
1
+ module HelperClasses
2
+ module ReadConfig
3
+ extend self
4
+
5
+ # Searches in this order:
6
+ # ~/.config
7
+ # ~
8
+ # /etc
9
+ #
10
+ # Returns nil if nothing found
11
+ def file_name(file)
12
+ %w( ~/.config ~ /etc ).each { |d|
13
+ file_abs = File.expand_path("#{d}/#{file}")
14
+ File.exists?(file_abs) and return file_abs
15
+ }
16
+ nil
17
+ end
18
+
19
+ # Very simple bash-reader, doesn't do array or multi-line configurations
20
+ def bash(file, downcase = false)
21
+ return nil unless File.exists? file
22
+ IO.readlines(file).collect { |l|
23
+ if l =~ /^#/
24
+ nil
25
+ elsif l =~ /([^ ]+)=(.*)/
26
+ [(downcase ? $1.downcase : $1).to_sym, $2]
27
+ end
28
+ }.compact.to_h
29
+ end
30
+
31
+ # Ruby file-reader, returns created hash
32
+ # THIS IS ABSOLUTELY INSECURE AND WILL EAT YOUR KITTENS!
33
+ # It returns what the file returns at the end - so most probably you'll want
34
+ # something like
35
+ #
36
+ # { one: 1,
37
+ # two: 2 }
38
+ #
39
+ # in that config-file
40
+ def ruby(file)
41
+ return {} unless File.exists? file.to_s
42
+ return eval(IO.readlines(file).join)
43
+ end
44
+
45
+ def json(file)
46
+ p 'Not implemented yet'
47
+ exit
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,104 @@
1
+ require 'helperclasses/dputs'
2
+ require 'helperclasses/system'
3
+
4
+ module HelperClasses
5
+ module Service
6
+ attr_accessor :system, :services
7
+
8
+ extend self
9
+ extend HelperClasses::DPuts
10
+ @system = case System.run_str 'uname -a'
11
+ when /ARCH/
12
+ :ArchLinux
13
+ when /Ubuntu/
14
+ :Ubuntu
15
+ when /Darwin/
16
+ :MacOSX
17
+ else
18
+ nil
19
+ end
20
+
21
+ @services = {
22
+ samba: {ArchLinux: %w( smbd nmbd ), Ubuntu: %w(smbd nmbd)},
23
+ cups: {ArchLinux: 'org.cups.cupsd', Ubuntu: 'cupsd'}
24
+ }
25
+
26
+ def service_get(service)
27
+ begin
28
+ @services[service.to_sym][@system]
29
+ rescue NoMethodError => _e
30
+ service.to_s
31
+ end
32
+ end
33
+
34
+ def service_run(service, cmd)
35
+ return unless @system
36
+ if !cmd
37
+ log_msg :Services, "System #{@system} can't start services"
38
+ return false
39
+ end
40
+ service_name = service_get(service)
41
+ if !service_name
42
+ log_msg :Services, "System #{@system} doesn't have service #{service}"
43
+ return false
44
+ end
45
+ cmd_system = cmd[@system]
46
+ if !cmd_system
47
+ log_msg :Services, "System #{@system} doesn't know how to do #{cmd}"
48
+ return false
49
+ end
50
+ [service_name].flatten.each { |s|
51
+ c = cmd_system.sub(/##/, s)
52
+ if !System.run_bool(c)
53
+ log_msg :Services, "Command #{c} failed"
54
+ return false
55
+ end
56
+ }
57
+ end
58
+
59
+ def start(service)
60
+ service_run(service, {ArchLinux: 'systemctl start ##',
61
+ Ubuntu: '/etc/init.d/## start',
62
+ MacOSX: nil}
63
+ )
64
+ end
65
+
66
+ def stop(service)
67
+ service_run(service, {ArchLinux: 'systemctl stop ##',
68
+ Ubuntu: '/etc/init.d/## stop',
69
+ MacOSX: nil}
70
+ )
71
+ end
72
+
73
+ def restart(service)
74
+ service_run(service, {ArchLinux: 'systemctl restart ##',
75
+ Ubuntu: '/etc/init.d/## restart',
76
+ MacOSX: nil}
77
+ )
78
+ end
79
+
80
+ def enable(service)
81
+ service_run(service, {ArchLinux: 'systemctl enable ##',
82
+ Ubuntu: nil,
83
+ MacOSX: nil}
84
+ )
85
+ end
86
+
87
+ def disable(service)
88
+ service_run(service, {ArchLinux: 'systemctl disable ##',
89
+ Ubuntu: nil,
90
+ MacOSX: nil}
91
+ )
92
+ end
93
+
94
+ def enable_start(service)
95
+ enable(service)
96
+ start(service)
97
+ end
98
+
99
+ def stop_disable(service)
100
+ disable(service)
101
+ stop(service)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,53 @@
1
+ # System-interaction for different flavours of Unix
2
+ require 'helperclasses/dputs'
3
+
4
+ module HelperClasses
5
+ module System
6
+ extend self
7
+ extend HelperClasses::DPuts
8
+ include HelperClasses::DPuts
9
+
10
+ def run_str(cmd)
11
+ dputs(3) { "Running command --#{cmd}--" }
12
+ %x[ #{cmd} ]
13
+ end
14
+
15
+ def run_bool(cmd)
16
+ dputs(3) { "Running command --#{cmd}--" }
17
+ Kernel.system("#{cmd} > /dev/null 2>&1")
18
+ end
19
+
20
+ def exists?(cmd)
21
+ dputs(3) { "Exist command --#{cmd}--?" }
22
+ run_bool("which #{cmd} > /dev/null 2>&1")
23
+ end
24
+
25
+ def rescue_all(msg = 'Error')
26
+ begin
27
+ yield
28
+ rescue Exception => e
29
+ dputs(0) { "#{Time.now.strftime('%a %y.%m.%d-%H:%M:%S')} - #{msg}" }
30
+ dputs(0) { "#{e.inspect}" }
31
+ dputs(0) { "#{e.to_s}" }
32
+ e.backtrace.each { |l| dputs(0) { l } }
33
+ end
34
+ end
35
+
36
+ def iptables(*args)
37
+ if !@iptables_cmd
38
+ if System.exists?('iptables')
39
+ @iptables_cmd = 'iptables'
40
+ @iptables_wait = (System.run_str('iptables --help') =~ /\s-w\s/) ? '-w' : ''
41
+ else
42
+ @iptables_cmd = ''
43
+ end
44
+ end
45
+
46
+ if @iptables_cmd != ''
47
+ System.run_str("#{@iptables_cmd} #{@iptables_wait} #{args.join(' ')}")
48
+ else
49
+ return ''
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ module HelperClasses
2
+ class Timing
3
+ def initialize(dbg = 0)
4
+ @dbg_lvl = dbg
5
+ @time = Time.now
6
+ end
7
+
8
+ def probe(msg = '')
9
+ t = sprintf('%6f', (Time.now - @time).to_f)
10
+ dputs(@dbg_lvl) { "#{msg}: #{t}" }
11
+ @time = Time.now
12
+ end
13
+
14
+ def self.measure(msg = '', dbg = 0)
15
+ t = Timing.new(dbg)
16
+ ret = yield
17
+ t.probe(msg)
18
+ ret
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ require 'helperclasses/arraysym'
2
+ require 'helperclasses/dputs'
3
+ require 'helperclasses/hashaccessor'
4
+ require 'helperclasses/readconfig'
5
+ require 'helperclasses/service'
6
+ require 'helperclasses/system'
7
+ require 'helperclasses/timing'
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ class TC_Service < Test::Unit::TestCase
5
+
6
+ def setup
7
+ end
8
+
9
+ def test_service_get
10
+ Service.system = :ArchLinux
11
+
12
+ assert_equal %w(smbd nmbd), Service.service_get(:samba)
13
+ assert_equal 'dnsmasq', Service.service_get(:dnsmasq)
14
+ end
15
+ end
data/test/test.rb ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.push '../lib', '.'
3
+
4
+ require 'test/unit'
5
+ require 'helperclasses'
6
+ include HelperClasses
7
+
8
+ tests = Dir.glob( 'hc_*.rb' )
9
+ #tests = %w( )
10
+
11
+ tests.each{|t|
12
+ begin
13
+ require "hc_#{t}"
14
+ rescue LoadError => e
15
+ require t
16
+ end
17
+ }
@@ -0,0 +1,5 @@
1
+ require 'helperclasses/arraysym'
2
+
3
+ using HelperClasses::ArraySym
4
+
5
+ p ["a", "b"].to_sym
@@ -0,0 +1,12 @@
1
+ require "helperclasses/dputs"
2
+
3
+ DEBUG_LVL = 5
4
+
5
+ include HelperClasses
6
+
7
+ DPuts.dputs(5){"Hello"}
8
+
9
+ include DPuts
10
+
11
+ dputs( 5 ){"Hello there"}
12
+
@@ -0,0 +1,9 @@
1
+ require 'helperclasses/hashaccessor'
2
+
3
+ using HelperClasses::HashAccessor
4
+
5
+ a = { :a => 3, "b" => 4 }
6
+
7
+ p a.to_sym
8
+ p a._a
9
+ p a._b
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.push('../lib')
4
+ require 'helperclasses/readconfig'
5
+
6
+ include HelperClasses
7
+
8
+ testfile = 'test_config'
9
+ IO.write(testfile, "#!/bin/bash
10
+ # This is an example config
11
+ TEST=hi
12
+ DB=foo.db
13
+ # This shouldn't pass
14
+ TEST2 = hi
15
+ # And some unrelated stuff
16
+ if '$1'; then
17
+ fi
18
+ ")
19
+
20
+ test = ReadConfig.bash( testfile )
21
+ p test
22
+
23
+ def printit
24
+ p test
25
+ end
26
+
27
+ printit
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.push '../lib'
4
+
5
+ require 'helperclasses/system'
6
+ include HelperClasses::System
7
+
8
+ rescue_all do
9
+ puts 'Hello there'
10
+ end
11
+ puts
12
+
13
+ rescue_all do
14
+ puts "Some math: #{10/0}"
15
+ end
16
+ puts
17
+
18
+ rescue_all('math-error') do
19
+ puts "Some math: #{10/0}"
20
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: helper_classes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Linus Gasser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Added accessors to Hash, to_sym to Array and a nice debugging-interface
14
+ called DPuts
15
+ email: ineiti@linusetviviane.ch
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - LICENSE
22
+ - README.md
23
+ - helper_classes.gemspec
24
+ - lib/helperclasses.rb
25
+ - lib/helperclasses/arraysym.rb
26
+ - lib/helperclasses/dputs.rb
27
+ - lib/helperclasses/hashaccessor.rb
28
+ - lib/helperclasses/readconfig.rb
29
+ - lib/helperclasses/service.rb
30
+ - lib/helperclasses/system.rb
31
+ - lib/helperclasses/timing.rb
32
+ - test/hc_service.rb
33
+ - test/test.rb
34
+ - test/test_arraysym.rb
35
+ - test/test_dputs.rb
36
+ - test/test_hashaccessor.rb
37
+ - test/test_readconfig.rb
38
+ - test/test_system.rb
39
+ homepage: https://github.com/ineiti/HelperClasses
40
+ licenses:
41
+ - GPLv3
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 2.2.2
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Hash._accessor Array.to_sym and DPuts
63
+ test_files:
64
+ - test/hc_service.rb
65
+ - test/test.rb
66
+ - test/test_arraysym.rb
67
+ - test/test_dputs.rb
68
+ - test/test_hashaccessor.rb
69
+ - test/test_readconfig.rb
70
+ - test/test_system.rb