clicoder 0.0.1

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,190 @@
1
+ require 'thor'
2
+ require 'thor/group'
3
+ require 'launchy'
4
+
5
+ require 'clicoder/judge'
6
+ require 'clicoder/site_base'
7
+ require 'clicoder/sites/sample_site'
8
+ require 'clicoder/sites/aoj'
9
+ require 'clicoder/sites/atcoder'
10
+
11
+ module Clicoder
12
+ class Starter < Thor
13
+ desc "sample_site", "Prepare directory to deal with new problem from SampleSite"
14
+ def sample_site
15
+ sample_site = SampleSite.new
16
+ start_with(sample_site)
17
+ end
18
+
19
+ desc "aoj PROBLEM_NUMBER", "Prepare directory to deal with new problem from AOJ"
20
+ def aoj(problem_number)
21
+ aoj = AOJ.new(problem_number)
22
+ start_with(aoj)
23
+ end
24
+
25
+ desc "atcoder CONTEST_ID PROBLEM_NUMBER", "Prepare directory to deal with new problem from AtCoder"
26
+ def atcoder(contest_id, problem_number)
27
+ atcoder = AtCoder.new(contest_id, problem_number)
28
+ start_with(atcoder)
29
+ end
30
+
31
+ no_commands do
32
+ def start_with(site)
33
+ site.start
34
+ puts "created directory #{site.working_directory}"
35
+ system("cd #{site.working_directory} && git init")
36
+ end
37
+ end
38
+ end
39
+
40
+ class CLI < Thor
41
+ desc "all", "build, execute, and judge"
42
+ def all
43
+ invoke :build
44
+ invoke :execute
45
+ invoke :judge
46
+ end
47
+
48
+ desc "build", "Build your program using `make build`"
49
+ def build
50
+ load_local_config
51
+ system('make build')
52
+ end
53
+
54
+ desc "execute", "Execute your program using `make execute`"
55
+ def execute
56
+ load_local_config
57
+ Dir.glob("#{INPUTS_DIRNAME}/*.txt").each do |input|
58
+ puts "executing #{input}"
59
+ FileUtils.cp(input, TEMP_INPUT_FILENAME)
60
+ system("make execute")
61
+ FileUtils.cp(TEMP_OUTPUT_FILENAME, "#{MY_OUTPUTS_DIRNAME}/#{File.basename(input)}")
62
+ end
63
+ FileUtils.rm([TEMP_INPUT_FILENAME, TEMP_OUTPUT_FILENAME])
64
+ end
65
+
66
+ desc "judge", "Judge your outputs"
67
+ method_option :decimal, type: :numeric, aliases: '-d', desc: 'Decimal position of allowed absolute error'
68
+ def judge
69
+ load_local_config
70
+ accepted = true
71
+ judge = Judge.new(options)
72
+ Dir.glob("#{OUTPUTS_DIRNAME}/*.txt").each do |output|
73
+ puts "judging #{output}"
74
+ my_output = "#{MY_OUTPUTS_DIRNAME}/#{File.basename(output)}"
75
+ if File.exists?(my_output)
76
+ unless judge.judge(output, my_output)
77
+ puts '! Wrong Answer'
78
+ system("diff -y #{output} #{my_output}")
79
+ accepted = false
80
+ end
81
+ else
82
+ puts "! #{my_output} does not exist"
83
+ accepted = false
84
+ end
85
+ end
86
+ if accepted
87
+ puts "Correct Answer"
88
+ else
89
+ puts "Wrong Answer"
90
+ end
91
+ end
92
+
93
+ desc "submit", "Submit your program"
94
+ def submit
95
+ load_local_config
96
+ site = get_site
97
+ if site.submit
98
+ puts "Submission Succeeded."
99
+ site.open_submission
100
+ else
101
+ puts "Submission Failed."
102
+ exit 1
103
+ end
104
+ end
105
+
106
+ desc "add_test", "Add new test case"
107
+ def add_test
108
+ load_local_config
109
+ test_count = Dir.glob("#{INPUTS_DIRNAME}/*.txt").count
110
+ input_file = "#{INPUTS_DIRNAME}/#{test_count}.txt"
111
+ output_file = "#{OUTPUTS_DIRNAME}/#{test_count}.txt"
112
+ puts 'Input:'
113
+ system("cat > #{input_file}")
114
+ puts 'Output:'
115
+ system("cat > #{output_file}")
116
+ end
117
+
118
+ desc "download", "Download description, inputs and outputs"
119
+ def download
120
+ load_local_config
121
+ site = get_site
122
+ # TODO: this is not beautiful
123
+ Dir.chdir('..') do
124
+ site.download_description
125
+ site.download_inputs
126
+ site.download_outputs
127
+ end
128
+ end
129
+
130
+ desc "browse", "Open problem page with the browser"
131
+ def browse
132
+ load_local_config
133
+ site = get_site
134
+ Launchy.open(site.problem_url)
135
+ end
136
+
137
+ no_commands do
138
+ def load_local_config
139
+ unless File.exists?('.config.yml')
140
+ puts 'It seems you are not in probelm directory'
141
+ exit 1
142
+ end
143
+ @local_config = YAML::load_file('.config.yml')
144
+ end
145
+
146
+ def get_site
147
+ SiteBase.new_with_config(@local_config)
148
+ end
149
+ end
150
+ register Starter, 'new', 'new <command>', 'start a new problem'
151
+ end
152
+
153
+ # Use aruba in-process
154
+ # https://github.com/cucumber/aruba
155
+ # https://github.com/erikhuda/thor/wiki/Integrating-with-Aruba-In-Process-Runs
156
+ class ArubaCLI
157
+ def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
158
+ @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
159
+ end
160
+
161
+ def execute!
162
+ exit_code = begin
163
+ # Thor accesses these streams directly rather than letting them be injected, so we replace them...
164
+ $stderr = @stderr
165
+ $stdin = @stdin
166
+ $stdout = @stdout
167
+
168
+ # Run our normal Thor app the way we know and love.
169
+ CLI.start(@argv)
170
+
171
+ # Thor::Base#start does not have a return value, assume success if no exception is raised.
172
+ 0
173
+ rescue Exception => e
174
+ # Proxy any exception that comes out of Thor itself back to stderr
175
+ $stderr.write(e.message + "\n")
176
+
177
+ # Exit with a failure code.
178
+ 1
179
+ ensure
180
+ # ...then we put them back.
181
+ $stderr = STDERR
182
+ $stdin = STDERR
183
+ $stdout = STDERR
184
+ end
185
+
186
+ # Proxy our exit code back to the injected kernel.
187
+ @kernel.exit(exit_code)
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,50 @@
1
+ require 'clicoder'
2
+
3
+ module Clicoder
4
+ class Config
5
+ attr_accessor :global, :local
6
+
7
+ def initialize
8
+ global_config_file = "#{global_config_dir}/config.yml"
9
+ @global = File.exists?(global_config_file) ? YAML::load_file(global_config_file) : {}
10
+ local_config_file = '.config.yml'
11
+ @local = File.exists?(local_config_file) ? YAML::load_file(local_config_file) : {}
12
+ end
13
+
14
+ # NOTE: This is not a class variable in order to evaluate stubbed ENV['HOME'] on each RSpec run
15
+ def global_config_dir
16
+ @global_config_dir ||= "#{ENV['HOME']}/.clicoder.d"
17
+ end
18
+
19
+ def asset(asset_name)
20
+ site_name = get('site')
21
+ file_name = get(site_name, asset_name)
22
+ if file_name.empty?
23
+ file_name = get('default', asset_name)
24
+ end
25
+
26
+ unless file_name.empty?
27
+ return File.expand_path(file_name, global_config_dir)
28
+ else
29
+ return ''
30
+ end
31
+ end
32
+
33
+ def merged_config
34
+ @merged_config ||= global.merge(local)
35
+ end
36
+
37
+ def get(*keys)
38
+ conf = merged_config
39
+ begin
40
+ keys.each do |key|
41
+ conf = conf[key]
42
+ end
43
+ return conf.nil? ? '' : conf
44
+ rescue
45
+ return ''
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module Clicoder
2
+ class Judge
3
+ def initialize(options)
4
+ @options = options
5
+ end
6
+
7
+ def judge(file1, file2)
8
+ if @options[:decimal]
9
+ float_judge(file1, file2, 10**(- @options[:decimal]))
10
+ else
11
+ diff_judge(file1, file2)
12
+ end
13
+ end
14
+
15
+ def diff_judge(file1, file2)
16
+ File.read(file1) == File.read(file2)
17
+ end
18
+
19
+ def float_judge(file1, file2, absolute_error)
20
+ lines1 = File.read(file1).split($/).map(&:strip)
21
+ floats1 = lines1.map{ |line| line.split(/\s+/).map(&:to_f) }.flatten
22
+ lines2 = File.read(file2).split($/).map(&:strip)
23
+ floats2 = lines2.map{ |line| line.split(/\s+/).map(&:to_f) }.flatten
24
+ floats1.zip(floats2).each do |float1, float2|
25
+ return false if (float1 - float2).abs >= absolute_error
26
+ end
27
+ true
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,131 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+ require 'yaml'
4
+ require 'net/http'
5
+ require 'abstract_method'
6
+ require 'reverse_markdown'
7
+
8
+ require 'clicoder'
9
+ require 'clicoder/config'
10
+
11
+ module Clicoder
12
+ class SiteBase
13
+ include Helper
14
+
15
+ # Parameters
16
+ abstract_method :site_name
17
+ abstract_method :problem_url
18
+ abstract_method :description_xpath
19
+ abstract_method :inputs_xpath
20
+ abstract_method :outputs_xpath
21
+ abstract_method :working_directory
22
+
23
+ # Operations
24
+ abstract_method :login
25
+ abstract_method :submit
26
+ abstract_method :open_submission
27
+
28
+ def self.new_with_config(config)
29
+ case config['site']
30
+ when 'sample_site'
31
+ SampleSite.new
32
+ when 'aoj'
33
+ AOJ.new(config['problem_number'])
34
+ when 'atcoder'
35
+ AtCoder.new(config['contest_id'], config['problem_number'])
36
+ end
37
+ end
38
+
39
+ def start
40
+ prepare_directories
41
+ login do
42
+ download_description
43
+ download_inputs
44
+ download_outputs
45
+ end
46
+ copy_template
47
+ copy_makefile
48
+ store_local_config
49
+ end
50
+
51
+ def prepare_directories
52
+ FileUtils.mkdir_p(working_directory)
53
+ Dir.chdir(working_directory) do
54
+ FileUtils.mkdir_p(INPUTS_DIRNAME)
55
+ FileUtils.mkdir_p(OUTPUTS_DIRNAME)
56
+ FileUtils.mkdir_p(MY_OUTPUTS_DIRNAME)
57
+ end
58
+ end
59
+
60
+ def download_description
61
+ Dir.chdir(working_directory) do
62
+ File.open('description.md', 'w') do |f|
63
+ f.write(ReverseMarkdown.parse(fetch_description))
64
+ end
65
+ end
66
+ end
67
+
68
+ def download_inputs
69
+ Dir.chdir("#{working_directory}/#{INPUTS_DIRNAME}") do
70
+ fetch_inputs.each_with_index do |input, i|
71
+ File.open("#{i}.txt", 'w') do |f|
72
+ f.write(input.strip + "\n")
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def download_outputs
79
+ Dir.chdir("#{working_directory}/#{OUTPUTS_DIRNAME}") do
80
+ fetch_outputs.each_with_index do |output, i|
81
+ File.open("#{i}.txt", 'w') do |f|
82
+ f.write(output.strip + "\n")
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def copy_template
89
+ template_file = config.asset('template')
90
+ return unless File.file?(template_file)
91
+ ext = File.extname(template_file)
92
+ FileUtils.cp(template_file, "#{working_directory}/main#{ext}")
93
+ end
94
+
95
+ def copy_makefile
96
+ makefile = config.asset('makefile')
97
+ return unless File.file?(makefile)
98
+ ext = File.extname(makefile)
99
+ FileUtils.cp(makefile, "#{working_directory}/Makefile")
100
+ end
101
+
102
+ def fetch_description
103
+ xml_document.at_xpath(description_xpath)
104
+ end
105
+
106
+ def fetch_inputs
107
+ input_nodes = xml_document.xpath(inputs_xpath)
108
+ input_nodes.map(&:text)
109
+ end
110
+
111
+ def fetch_outputs
112
+ outputs_nodes = xml_document.xpath(outputs_xpath)
113
+ outputs_nodes.map(&:text)
114
+ end
115
+
116
+ def store_local_config
117
+ config.local['site'] = site_name
118
+ File.open("#{working_directory}/.config.yml", 'w') do |f|
119
+ f.write(config.local.to_yaml)
120
+ end
121
+ end
122
+
123
+ def xml_document
124
+ @xml_document ||= Nokogiri::HTML(open(problem_url))
125
+ end
126
+
127
+ def config
128
+ @config ||= Config.new
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,59 @@
1
+ require 'clicoder/site_base'
2
+ require 'clicoder/config'
3
+
4
+ module Clicoder
5
+ class AOJ < SiteBase
6
+
7
+ def initialize(problem_number)
8
+ config.local['problem_number'] = problem_number
9
+ @problem_id = "%04d" % problem_number
10
+ end
11
+
12
+ def submit
13
+ submit_url = 'http://judge.u-aizu.ac.jp/onlinejudge/servlet/Submit'
14
+ post_params = {
15
+ userID: config.get('aoj', 'user_id'),
16
+ password: config.get('aoj', 'password'),
17
+ problemNO: @problem_id,
18
+ language: ext_to_language_name(File.extname(detect_main)),
19
+ sourceCode: File.read(detect_main),
20
+ submit: 'Send'
21
+ }
22
+ response = Net::HTTP.post_form(URI(submit_url), post_params)
23
+ return response.body !~ /UserID or Password is Wrong/
24
+ end
25
+
26
+ def open_submission
27
+ Launchy.open('http://judge.u-aizu.ac.jp/onlinejudge/status.jsp')
28
+ end
29
+
30
+ def login
31
+ # no need to login for now
32
+ yield
33
+ end
34
+
35
+ def site_name
36
+ 'aoj'
37
+ end
38
+
39
+ def problem_url
40
+ "http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=#{@problem_id}"
41
+ end
42
+
43
+ def description_xpath
44
+ '//div[@class="description"]'
45
+ end
46
+
47
+ def inputs_xpath
48
+ '//*[self::pre or self::div/pre][preceding-sibling::*[self::h2 or self::h3][1][text()="Sample Input"]]'
49
+ end
50
+
51
+ def outputs_xpath
52
+ '//*[self::pre or self::div/pre][preceding-sibling::*[self::h2 or self::h3][text()="Output for the Sample Input"]]'
53
+ end
54
+
55
+ def working_directory
56
+ @problem_id
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,66 @@
1
+ require 'clicoder/site_base'
2
+ require 'clicoder/config'
3
+
4
+ require 'mechanize'
5
+
6
+ module Clicoder
7
+ class AtCoder < SiteBase
8
+
9
+ def initialize(contest_id, problem_number)
10
+ config.local['contest_id'] = contest_id
11
+ config.local['problem_number'] = problem_number
12
+ @contest_id = contest_id
13
+ @problem_id = "#{@contest_id}_#{problem_number}"
14
+ end
15
+
16
+ def submit
17
+ login do |mechanize, contest_page|
18
+ problem_page = mechanize.get(problem_url)
19
+ submit_page = problem_page.link_with(href: /submit/).click
20
+ submit_page.form_with(action: /submit/) do |f|
21
+ f.field_with(name: 'source_code').value = File.read(detect_main)
22
+ end.click_button
23
+ end
24
+ end
25
+
26
+ def open_submission
27
+ Launchy.open("http://#{@contest_id}.contest.atcoder.jp/submissions/me")
28
+ end
29
+
30
+ def login
31
+ Mechanize.start do |m|
32
+ login_page = m.get("http://#{@contest_id}.contest.atcoder.jp/login")
33
+ contest_home_page = login_page.form_with(action: '/login') do |f|
34
+ f.field_with(name: 'name').value = config.get('atcoder', 'user_id')
35
+ f.field_with(name: 'password').value = config.get('atcoder', 'password')
36
+ end.click_button
37
+
38
+ yield m, contest_home_page
39
+ end
40
+ end
41
+
42
+ def site_name
43
+ 'atcoder'
44
+ end
45
+
46
+ def problem_url
47
+ "http://#{@contest_id}.contest.atcoder.jp/tasks/#{@problem_id}"
48
+ end
49
+
50
+ def description_xpath
51
+ '//div[@id="task-statement"]'
52
+ end
53
+
54
+ def inputs_xpath
55
+ '//pre[preceding-sibling::h3[1][contains(text(), "入力例")]]'
56
+ end
57
+
58
+ def outputs_xpath
59
+ '//pre[preceding-sibling::h3[1][contains(text(), "出力例")]]'
60
+ end
61
+
62
+ def working_directory
63
+ @problem_id
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,52 @@
1
+ require 'clicoder/site_base'
2
+ require 'clicoder/config'
3
+
4
+ require 'net/http'
5
+ require 'launchy'
6
+
7
+ module Clicoder
8
+ class SampleSite < SiteBase
9
+
10
+ def submit
11
+ submit_url = 'http://samplesite.com/submit'
12
+ post_params = {
13
+ user_id: config.get('sample_site', 'user_id'),
14
+ password: config.get('sample_site', 'password'),
15
+ }
16
+ response = Net::HTTP.post_form(URI(submit_url), post_params)
17
+ return response.body =~ /Success/
18
+ end
19
+
20
+ def open_submission
21
+ Launchy.open('http://samplesite.com/submissions')
22
+ end
23
+
24
+ def login
25
+ yield
26
+ end
27
+
28
+ def site_name
29
+ 'sample_site'
30
+ end
31
+
32
+ def problem_url
33
+ "#{GEM_ROOT}/fixtures/sample_problem.html"
34
+ end
35
+
36
+ def description_xpath
37
+ '//div[@id="description"]'
38
+ end
39
+
40
+ def inputs_xpath
41
+ '//div[@id="inputs"]/pre'
42
+ end
43
+
44
+ def outputs_xpath
45
+ '//div[@id="outputs"]/pre'
46
+ end
47
+
48
+ def working_directory
49
+ 'working_directory'
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module Clicoder
2
+ VERSION = "0.0.1"
3
+ end
data/lib/clicoder.rb ADDED
@@ -0,0 +1,32 @@
1
+ require "clicoder/version"
2
+
3
+ module Clicoder
4
+ GEM_ROOT = Gem::Specification.find_by_name('clicoder').gem_dir
5
+
6
+ INPUTS_DIRNAME = 'inputs'
7
+ OUTPUTS_DIRNAME = 'outputs'
8
+ MY_OUTPUTS_DIRNAME = 'my_outputs'
9
+ TEMP_INPUT_FILENAME = 'in.txt'
10
+ TEMP_OUTPUT_FILENAME = 'out.txt'
11
+
12
+ module Helper
13
+ def detect_main
14
+ Dir.glob('main.*').first
15
+ end
16
+
17
+ def ext_to_language_name(ext)
18
+ @map ||= {
19
+ cpp: 'C++',
20
+ cc: 'C++',
21
+ c: 'C',
22
+ java: 'JAVA',
23
+ cs: 'C#',
24
+ d: 'D',
25
+ rb: 'Ruby',
26
+ py: 'Python',
27
+ php: 'PHP'
28
+ }
29
+ return @map[ext.gsub(/^\./, '').to_sym]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ require 'clicoder'
4
+ require 'clicoder/config'
5
+
6
+ module Clicoder
7
+ describe Config do
8
+ let(:config) { Config.new }
9
+ let(:global_config) { YAML::load_file(global_config_file) }
10
+ let(:global_config_dir) { "#{ENV['HOME']}/.clicoder.d" }
11
+ let(:global_config_file) { "#{global_config_dir}/config.yml" }
12
+ let(:local_config) { { 'site' => 'sample_site' } }
13
+ let(:local_config_file) { '.config.yml' }
14
+
15
+ before do
16
+ File.open(local_config_file, 'w') do |f|
17
+ f.write(local_config.to_yaml)
18
+ end
19
+ end
20
+
21
+ describe '.new' do
22
+ it 'loads global configuration from global_config_file' do
23
+ expect(config.global).to eql(global_config)
24
+ end
25
+
26
+ it 'loads local configuration from local_config_file' do
27
+ expect(config.local).to eql(local_config)
28
+ end
29
+ end
30
+
31
+ describe '#asset' do
32
+ context 'when the site specific asset is specified in the config file' do
33
+ it 'returns site specific template' do
34
+ site = local_config['site']
35
+ file_name = global_config[site]['template']
36
+ expect(config.asset('template')).to eql(File.expand_path(file_name, global_config_dir))
37
+ end
38
+ end
39
+
40
+ context 'when the site specific asset is not specified in the config file' do
41
+ before do
42
+ config.global[local_config['site']] = {}
43
+ end
44
+
45
+ it 'returns default template' do
46
+ file_name = global_config['default']['template']
47
+ expect(config.asset('template')).to eql(File.expand_path(file_name, global_config_dir))
48
+ end
49
+ end
50
+
51
+ context 'when nothing is specified in the config file' do
52
+ before do
53
+ config.global = {}
54
+ end
55
+
56
+ it 'returns empty string' do
57
+ expect(config.asset('template')).to eql('')
58
+ end
59
+ end
60
+ end
61
+
62
+ describe '#get' do
63
+ context 'without arguments' do
64
+ it 'returns config.global.merge(config.local)' do
65
+ expect(config.get()).to eql(config.global.merge(config.local))
66
+ end
67
+ end
68
+
69
+ context 'with arguments' do
70
+ context 'when config is missing' do
71
+ it 'returns empty string' do
72
+ expect(config.get('it', 'is', 'missing')).to eql('')
73
+ end
74
+ end
75
+
76
+ context 'when config is present' do
77
+ it 'returns the config value' do
78
+ expect(config.get('sample_site', 'user_id')).to eql(global_config['sample_site']['user_id'])
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end