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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +202 -0
- data/Rakefile +1 -0
- data/bin/clicoder +4 -0
- data/clicoder.gemspec +35 -0
- data/features/sample_site.feature +79 -0
- data/features/step_definitions/sample_site_steps.rb +80 -0
- data/features/support/setup.rb +26 -0
- data/features/support/webmock.rb +5 -0
- data/fixtures/clicoder.d/Makefile +6 -0
- data/fixtures/clicoder.d/config.yml +9 -0
- data/fixtures/clicoder.d/template.cpp +6 -0
- data/fixtures/sample_problem.html +23 -0
- data/lib/clicoder/cli.rb +190 -0
- data/lib/clicoder/config.rb +50 -0
- data/lib/clicoder/judge.rb +30 -0
- data/lib/clicoder/site_base.rb +131 -0
- data/lib/clicoder/sites/aoj.rb +59 -0
- data/lib/clicoder/sites/atcoder.rb +66 -0
- data/lib/clicoder/sites/sample_site.rb +52 -0
- data/lib/clicoder/version.rb +3 -0
- data/lib/clicoder.rb +32 -0
- data/spec/config_spec.rb +84 -0
- data/spec/judge_spec.rb +64 -0
- data/spec/site_base_spec.rb +41 -0
- data/spec/sites/aoj_spec.rb +75 -0
- data/spec/sites/sample_site_spec.rb +117 -0
- data/spec/spec_helper.rb +21 -0
- metadata +268 -0
data/lib/clicoder/cli.rb
ADDED
@@ -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
|
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
|
data/spec/config_spec.rb
ADDED
@@ -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
|