mc-limit 1.0.0-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mc-limit.gemspec
4
+ gemspec
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mc-limit (0.0.1-x86-mingw32)
5
+ sys-proctable
6
+ win32-api
7
+ wxruby-ruby19
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ sys-proctable (0.9.1-x86-mingw32)
13
+ win32-api (1.4.8-x86-mingw32)
14
+ wxruby-ruby19 (2.0.1-x86-mingw32)
15
+
16
+ PLATFORMS
17
+ x86-mingw32
18
+
19
+ DEPENDENCIES
20
+ mc-limit!
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Tim Jensen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,71 @@
1
+ MC-Limit
2
+ ========
3
+
4
+ Minecraft Limiter (mc-limit) launches Minecraft in offline mode
5
+ and automatically terminates the game after it has been played for
6
+ a pre-determined amount of time.
7
+
8
+ ## Requirements
9
+
10
+ * Minecraft (http://minecraft.net/download)
11
+ * Java VM (http://java.com/download)
12
+ * Ruby 1.9.x (http://ruby-lang.org/en/downloads)
13
+
14
+ ## How to install it
15
+
16
+ TODO
17
+
18
+ ## How to configure it
19
+
20
+ The utility uses environment variables for its configuration.
21
+
22
+ ### Default time limit
23
+
24
+ Set the `DEFAULT_MC_LIMIT` variable to equal the default number of
25
+ minutes of Minecraft play to allow per day. If the variable is not
26
+ set, the default time limit will be 30 minutes.
27
+
28
+ ### Remaining time file
29
+
30
+ Set the `MC_LIMIT_FILE` variable to the full pathname of the file
31
+ that is used to store the remaining play time between runs. If the
32
+ variable is not set, the default file is:
33
+
34
+ - Windows: `%APPDATA%\.mc-limit\remaining.yml`
35
+ - Others: `$HOME/.mc-limit/remaining.yml`
36
+
37
+ ### Admin password
38
+
39
+ Set the `MC_LIMIT_ADMIN_PASSWORD` variable to be the plain text
40
+ password required in order to run the administration tool. If this
41
+ variable is not set, the administration tool will not run.
42
+
43
+ ### Game command
44
+
45
+ Set the `MC_LIMIT_COMMAND` variable to the complete command line
46
+ used to launch Minecraft.
47
+
48
+ ## How to run it
49
+
50
+ Run "mc-limit" to launch Minecraft. I recommend creating a desktop
51
+ shortcut to make it easier to run.
52
+
53
+ ### Administration tool
54
+
55
+ Run "mc-limit-admin" to launch the administration tool. This tool
56
+ can be used to add or subtract minutes from today's limit.
57
+
58
+ ## Security
59
+
60
+ This utility is by no means hacker-proof. Any curious child (or
61
+ adult) who is literate and moderately skilled with computers is
62
+ capable of discovering how to defeat the time limit. I consider it
63
+ a challenge for my kids.
64
+
65
+ ## To-Do List
66
+
67
+ * Make it portable
68
+ * Display a timer while Minecraft is running that shows how much play
69
+ time is left.
70
+ * Improve security.
71
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $0 = 'mc-limit'
4
+
5
+ begin
6
+ require 'mc-limit'
7
+ rescue LoadError
8
+ require 'rubygems'
9
+ require 'mc-limit'
10
+ end
11
+
12
+ MCLimit.launch
13
+
14
+ # vim:ts=2:sw=2:et:ft=ruby
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $0 = 'mc-limit'
4
+
5
+ begin
6
+ require 'mc-limit'
7
+ rescue LoadError
8
+ require 'rubygems'
9
+ require 'mc-limit'
10
+ end
11
+
12
+ MCLimit.admin
13
+
14
+ # vim:ts=2:sw=2:et:ft=ruby
@@ -0,0 +1,7 @@
1
+ require 'mc-limit/admin'
2
+ require 'mc-limit/core'
3
+ require 'mc-limit/gui'
4
+ require 'mc-limit/version'
5
+ require 'mc-limit/winfns' if RUBY_PLATFORM =~ /mingw/
6
+
7
+ # vim:ts=2:sw=2:et
@@ -0,0 +1,59 @@
1
+ require 'wx'
2
+
3
+ module MCLimit
4
+ class AdminDialog < Wx::Dialog
5
+ def initialize(parent, value)
6
+ super( parent, -1, 'Minecraft Limiter Administration' )
7
+
8
+ sizer = Wx::FlexGridSizer.new( 1, 2, 0, 0 )
9
+ label = Wx::StaticText.new( self, -1, 'Minutes of game play remaining:' )
10
+ sizer.add( label, 0, Wx::ALL, 4 )
11
+ @expand = Wx::SpinCtrl.new( self )
12
+ @expand.set_range(0, 1440)
13
+ @expand.set_value(value)
14
+ @expand.set_selection(-1, -1)
15
+ sizer.add( @expand, 0, Wx::ALL, 4 )
16
+
17
+ topsizer = Wx::GridSizer.new( 2, 1, 0, 0 )
18
+ topsizer.add( sizer, 0, Wx::ALL, 4 )
19
+ topsizer.add( create_std_dialog_button_sizer( Wx::OK | Wx::CANCEL ), 0, Wx::ALL, 4 )
20
+ self.set_sizer( topsizer )
21
+ topsizer.set_size_hints( self )
22
+ topsizer.fit( self )
23
+ @expand.set_focus
24
+ end
25
+
26
+ def value
27
+ @expand.get_value
28
+ end
29
+ end
30
+
31
+ def self.check_admin_password( parent )
32
+ raise 'Administration password has not been set!' if MCLimit.admin_password.nil?
33
+ password = Wx::PasswordEntryDialog.new( parent, 'Enter administration password' )
34
+ raise 'Password required!' unless Wx::ID_OK == password.show_modal
35
+ raise 'Incorrect password!' unless password.get_value == MCLimit.admin_password
36
+ end
37
+
38
+ class AdminApp < Wx::App
39
+ def on_init
40
+ frame = Wx::Frame.new(nil, -1, 'Dialog')
41
+ begin
42
+ MCLimit.check_admin_password( frame )
43
+ admin = AdminDialog.new( frame, MCLimit.remaining_minutes.to_i )
44
+ MCLimit.update_remaining_minutes( admin.value ) if Wx::ID_OK == admin.show_modal
45
+ rescue => e
46
+ GUI.error( e.message )
47
+ raise if $DEBUG
48
+ ensure
49
+ frame.close
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.admin
55
+ AdminApp.new.main_loop
56
+ end
57
+ end
58
+
59
+ # vim:ts=2:sw=2:et
@@ -0,0 +1,106 @@
1
+ require 'date'
2
+ require 'yaml'
3
+ require 'fileutils'
4
+ require 'sys/proctable'
5
+
6
+ module MCLimit
7
+ # The default number of minutes of game play to allow per day when env not set
8
+ DEFAULT_MINUTES = 30
9
+
10
+ # The minimum number of remaining minutes needed to start the game
11
+ MINIMUM_MINUTES = 1
12
+
13
+ REMAINING_FILE = 'remaining.yml'
14
+
15
+ GAME_COMMAND = 'javaw.exe -Xms512m -Xmx1024m -cp "%APPDATA%\.minecraft\bin\*" -Djava.library.path="%APPDATA%\.minecraft\bin\natives" net.minecraft.client.Minecraft'
16
+
17
+ def self.disappoint( title, body )
18
+ GUI.error( body, title )
19
+ exit 1
20
+ end
21
+
22
+ def self.game_command
23
+ ENV['MC_LIMIT_COMMAND'] || GAME_COMMAND
24
+ end
25
+
26
+ def self.remaining_file
27
+ if ENV['MC_LIMIT_FILE'].nil?
28
+ if RUBY_PLATFORM =~ /mingw/
29
+ File.join( ENV['APPDATA'], '.mc-limit', REMAINING_FILE )
30
+ else
31
+ File.join( ENV['HOME'], '.mc-limit', REMAINING_FILE )
32
+ end
33
+ else
34
+ ENV['MC_LIMIT_FILE']
35
+ end
36
+ end
37
+
38
+ def self.admin_password
39
+ ENV['MC_LIMIT_ADMIN_PASSWORD']
40
+ end
41
+
42
+ def self.default_minutes
43
+ Float( ENV['DEFAULT_MC_LIMIT'] || DEFAULT_MINUTES )
44
+ end
45
+
46
+ def self.remaining_minutes( date = Date.today )
47
+ FileUtils.mkdir_p( File.dirname( MCLimit.remaining_file ) )
48
+ yaml = YAML.load_file( MCLimit.remaining_file )
49
+ ( yaml[:date] == date ) ? Float(yaml[:remaining]) : default_minutes
50
+ rescue
51
+ default_minutes
52
+ end
53
+
54
+ def self.update_remaining_minutes( minutes )
55
+ yaml = { :date => Date.today, :remaining => minutes }.to_yaml
56
+ FileUtils.mkdir_p( File.dirname( MCLimit.remaining_file ) )
57
+ File.open( MCLimit.remaining_file, 'wt' ) { |f| f.write yaml }
58
+ end
59
+
60
+ def self.stop_minecraft(pids)
61
+ if RUBY_PLATFORM =~ /mingw/
62
+ Win.close_process(pids, 'Minecraft')
63
+ else
64
+ pids.each { |pid| kill(:QUIT, pid) }
65
+ end
66
+ end
67
+
68
+ def self.timeout_pid(pid, minutes)
69
+ Thread.new do
70
+ sleep minutes * 60
71
+ pids = [ pid ]
72
+ Sys::ProcTable.ps do |process|
73
+ pids << process.pid if pids.include? process.ppid
74
+ end
75
+ MCLimit.stop_minecraft(pids)
76
+ end
77
+ pid
78
+ end
79
+
80
+ def self.validate_sufficient( time_limit )
81
+ return if time_limit >= MINIMUM_MINUTES
82
+ disappoint( 'Sorry', 'No more Minecraft allowed today!' )
83
+ end
84
+
85
+ def self.run( command, time_limit )
86
+ validate_sufficient time_limit
87
+ pid = timeout_pid( Process.spawn( command ), time_limit )
88
+ end
89
+
90
+ def self.launch
91
+ GUI.new.main_loop do
92
+ remaining = MCLimit.remaining_minutes
93
+ pid = MCLimit.run( MCLimit.game_command, remaining )
94
+
95
+ start = Time.now
96
+ Process.waitpid( pid, 0 )
97
+ finish = Time.now
98
+
99
+ consumed = ( finish - start ) / 60
100
+ remaining = [ 0, remaining - consumed ].sort.last
101
+ MCLimit.update_remaining_minutes( remaining )
102
+ end
103
+ end
104
+ end
105
+
106
+ # vim:ts=2:sw=2:et
@@ -0,0 +1,23 @@
1
+ require 'wx'
2
+
3
+ module MCLimit
4
+ class GUI < Wx::App
5
+ def on_init
6
+ frame = Wx::Frame.new(nil, -1, 'Hidden')
7
+ @main.call
8
+ ensure
9
+ frame.close
10
+ end
11
+ def main_loop(&main)
12
+ @main = main
13
+ super
14
+ end
15
+
16
+ def self.error( message, title = 'Error' )
17
+ dialog = Wx::MessageDialog.new( nil, message, title, Wx::OK | Wx::ICON_ERROR )
18
+ dialog.show_modal
19
+ end
20
+ end
21
+ end
22
+
23
+ # vim:ts=2:sw=2:et
@@ -0,0 +1,3 @@
1
+ module MCLimit
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'win32/api'
2
+
3
+ module MCLimit
4
+ module Win
5
+ WM_CLOSE = 0x10
6
+
7
+ SendMessage = Win32::API.new('SendMessage', 'LLLL', 'I', 'user32')
8
+ EnumWindows = Win32::API.new('EnumWindows', 'KP', 'L', 'user32')
9
+ GetWindowText = Win32::API.new('GetWindowText', 'LPI', 'I', 'user32')
10
+ GetWindowThreadProcessId = Win32::API.new('GetWindowThreadProcessId', 'LP', 'L', 'user32')
11
+
12
+ def self.window_for_pid(pid, text)
13
+ result = nil
14
+ finder = Win32::API::Callback.new('LP', 'I') do |handle, param|
15
+ pidp = [0].pack('L')
16
+ GetWindowThreadProcessId.call(handle, pidp)
17
+ if pid == pidp.unpack('L')[0]
18
+ buf = "\0" * 200
19
+ GetWindowText.call(handle, buf, 200)
20
+ result = handle if buf.strip == text
21
+ end
22
+ true
23
+ end
24
+ EnumWindows.call(finder, nil)
25
+ result
26
+ end
27
+
28
+ def self.close_process(pids, text)
29
+ pids.each do |pid|
30
+ handle = window_for_pid(pid, 'Minecraft')
31
+ next if handle.nil?
32
+ SendMessage.call(handle, WM_CLOSE, 0, 0)
33
+ break
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # vim:ts=2:sw=2:et
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/mc-limit/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Tim Jensen"]
6
+ gem.email = ["tim.l.jensen@gmail.com"]
7
+ gem.description = %q{Minecraft Limiter (mc-limit) launches Minecraft in offline mode and automatically terminates the game after it has been played for a pre-determined amount of time.}
8
+ gem.summary = %q{Limit the amount of time Minecraft can be played per day}
9
+ gem.homepage = "https://github.com/tjensen/mc-limit"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "mc-limit"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = MCLimit::VERSION
17
+
18
+ gem.platform = Gem::Platform::CURRENT
19
+ gem.required_ruby_version = ">= 1.9.1"
20
+
21
+ gem.add_dependency('sys-proctable')
22
+ gem.add_dependency('win32-api')
23
+ gem.add_dependency('wxruby-ruby19')
24
+ end
@@ -0,0 +1,84 @@
1
+ require 'test/unit'
2
+ require 'fileutils'
3
+ require 'mc-limit'
4
+
5
+ class TC_Default_Minutes < Test::Unit::TestCase
6
+ def setup
7
+ ENV.delete 'DEFAULT_MC_LIMIT'
8
+ end
9
+
10
+ def test_return_hardcoded_default_when_env_not_set
11
+ assert_equal MCLimit::DEFAULT_MINUTES, MCLimit.default_minutes
12
+ end
13
+
14
+ def test_return_env_value_when_set
15
+ ENV['DEFAULT_MC_LIMIT'] = '2112'
16
+ assert_equal 2112, MCLimit.default_minutes
17
+ end
18
+ end
19
+
20
+ class TC_Remaining_Minutes < Test::Unit::TestCase
21
+ def setup
22
+ FileUtils.rm_f( MCLimit::REMAINING_FILE )
23
+ ENV.delete 'DEFAULT_MC_LIMIT'
24
+ end
25
+ def teardown
26
+ FileUtils.rm_f( MCLimit::REMAINING_FILE )
27
+ end
28
+
29
+ def test_return_hardcoded_default_when_file_missing_and_env_not_set
30
+ assert_equal MCLimit::DEFAULT_MINUTES, MCLimit.remaining_minutes
31
+ end
32
+
33
+ def test_return_env_default_when_file_missing
34
+ ENV['DEFAULT_MC_LIMIT'] = '74'
35
+ assert_equal 74, MCLimit.remaining_minutes
36
+ end
37
+
38
+ def test_return_file_defined_minutes_if_file_contains_todays_date
39
+ yaml = { :date => Date.new(1776, 7, 4), :remaining => 13 }.to_yaml
40
+ File.open(MCLimit::REMAINING_FILE, 'wt') { |f| f.write( yaml ) }
41
+ assert_equal 13, MCLimit.remaining_minutes( Date.new(1776, 7, 4) )
42
+ end
43
+
44
+ def test_return_default_minutes_if_file_contains_different_date
45
+ yaml = { :date => Date.new(1776, 7, 4), :remaining => 13 }.to_yaml
46
+ File.open(MCLimit::REMAINING_FILE, 'wt') { |f| f.write( yaml ) }
47
+ assert_equal MCLimit.default_minutes, MCLimit.remaining_minutes
48
+ end
49
+
50
+ def test_return_default_minutes_if_file_contains_garbage_date
51
+ yaml = { :date => 'garbage', :remaining => 13 }.to_yaml
52
+ File.open(MCLimit::REMAINING_FILE, 'wt') { |f| f.write( yaml ) }
53
+ assert_equal MCLimit.default_minutes, MCLimit.remaining_minutes
54
+ end
55
+
56
+ def test_return_default_minutes_if_file_contains_garbage_minutes
57
+ yaml = { :date => Date.new(1776, 7, 4), :remaining => 'garbage' }.to_yaml
58
+ File.open(MCLimit::REMAINING_FILE, 'wt') { |f| f.write( yaml ) }
59
+ assert_equal MCLimit.default_minutes, MCLimit.remaining_minutes( Date.new(1776, 7, 4) )
60
+ end
61
+
62
+ def test_return_default_minutes_if_file_contains_garbage
63
+ File.open(MCLimit::REMAINING_FILE, 'wt') { |f| f.write( 'garbage' ) }
64
+ assert_equal MCLimit.default_minutes, MCLimit.remaining_minutes
65
+ end
66
+ end
67
+
68
+ class TC_Update_Remaining_Minutes < Test::Unit::TestCase
69
+ def setup
70
+ FileUtils.rm_f( MCLimit::REMAINING_FILE )
71
+ ENV.delete 'DEFAULT_MC_LIMIT'
72
+ end
73
+ def teardown
74
+ FileUtils.rm_f( MCLimit::REMAINING_FILE )
75
+ end
76
+
77
+ def test_writes_current_date_and_given_minutes_to_file
78
+ MCLimit.update_remaining_minutes 42
79
+ assert_equal( { :date => Date.today, :remaining => 42 },
80
+ YAML.load_file(MCLimit::REMAINING_FILE) )
81
+ end
82
+ end
83
+
84
+ # vim:ts=2:sw=2:et
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mc-limit
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: x86-mingw32
7
+ authors:
8
+ - Tim Jensen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sys-proctable
16
+ requirement: &20769408 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *20769408
25
+ - !ruby/object:Gem::Dependency
26
+ name: win32-api
27
+ requirement: &20769120 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *20769120
36
+ - !ruby/object:Gem::Dependency
37
+ name: wxruby-ruby19
38
+ requirement: &20768832 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *20768832
47
+ description: Minecraft Limiter (mc-limit) launches Minecraft in offline mode and automatically
48
+ terminates the game after it has been played for a pre-determined amount of time.
49
+ email:
50
+ - tim.l.jensen@gmail.com
51
+ executables:
52
+ - mc-limit
53
+ - mc-limit-admin
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - .gitignore
58
+ - Gemfile
59
+ - Gemfile.lock
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - bin/mc-limit
64
+ - bin/mc-limit-admin
65
+ - lib/mc-limit.rb
66
+ - lib/mc-limit/admin.rb
67
+ - lib/mc-limit/core.rb
68
+ - lib/mc-limit/gui.rb
69
+ - lib/mc-limit/version.rb
70
+ - lib/mc-limit/winfns.rb
71
+ - mc-limit.gemspec
72
+ - test/test_core.rb
73
+ homepage: https://github.com/tjensen/mc-limit
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: 1.9.1
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 1.8.16
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Limit the amount of time Minecraft can be played per day
97
+ test_files:
98
+ - test/test_core.rb