at_coder_friends 0.5.0 → 0.5.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 +4 -4
- data/.gitignore +5 -2
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +4 -1
- data/Gemfile.lock +16 -14
- data/Rakefile +2 -0
- data/at_coder_friends.gemspec +1 -0
- data/lib/at_coder_friends.rb +13 -5
- data/lib/at_coder_friends/cli.rb +1 -2
- data/lib/at_coder_friends/config_loader.rb +2 -2
- data/lib/at_coder_friends/context.rb +3 -3
- data/lib/at_coder_friends/cxx_generator.rb +7 -13
- data/lib/at_coder_friends/emitter.rb +0 -4
- data/lib/at_coder_friends/parser/constraints_parser.rb +26 -0
- data/lib/at_coder_friends/parser/format_parser.rb +154 -0
- data/lib/at_coder_friends/parser/main.rb +16 -0
- data/lib/at_coder_friends/parser/page_parser.rb +119 -0
- data/lib/at_coder_friends/path_util.rb +10 -0
- data/lib/at_coder_friends/problem.rb +11 -14
- data/lib/at_coder_friends/scraping/agent.rb +77 -0
- data/lib/at_coder_friends/scraping/authentication.rb +71 -0
- data/lib/at_coder_friends/scraping/custom_test.rb +53 -0
- data/lib/at_coder_friends/scraping/session.rb +26 -0
- data/lib/at_coder_friends/scraping/submission.rb +31 -0
- data/lib/at_coder_friends/scraping/tasks.rb +39 -0
- data/lib/at_coder_friends/test_runner/base.rb +123 -0
- data/lib/at_coder_friends/test_runner/judge.rb +44 -0
- data/lib/at_coder_friends/test_runner/sample.rb +35 -0
- data/lib/at_coder_friends/verifier.rb +4 -2
- data/lib/at_coder_friends/version.rb +1 -1
- data/tasks/regression.rake +163 -0
- metadata +30 -7
- data/lib/at_coder_friends/format_parser.rb +0 -151
- data/lib/at_coder_friends/judge_test_runner.rb +0 -34
- data/lib/at_coder_friends/sample_test_runner.rb +0 -31
- data/lib/at_coder_friends/scraping_agent.rb +0 -265
- data/lib/at_coder_friends/test_runner.rb +0 -104
@@ -1,151 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AtCoderFriends
|
4
|
-
# Iterates through elements of an array
|
5
|
-
class Iterator
|
6
|
-
def initialize(array)
|
7
|
-
@array = array
|
8
|
-
@i = 0
|
9
|
-
end
|
10
|
-
|
11
|
-
def next?
|
12
|
-
@i < @array.size
|
13
|
-
end
|
14
|
-
|
15
|
-
def next
|
16
|
-
ret = @array[@i]
|
17
|
-
@i += 1
|
18
|
-
ret
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# parses input data format and generates input definitons
|
23
|
-
class FormatParser
|
24
|
-
PARSERS = [
|
25
|
-
{
|
26
|
-
container: :harray,
|
27
|
-
item: :number,
|
28
|
-
pat: /^(?<v>[a-z]+)[01](\s+\k<v>.)*(\s+\.+)?(\s+\k<v>.)+$/i,
|
29
|
-
names: ->(m) { [m[:v]] },
|
30
|
-
pat2: ->(_) { nil },
|
31
|
-
size: ->(f) { [f[-1]] }
|
32
|
-
},
|
33
|
-
{
|
34
|
-
container: :harray,
|
35
|
-
item: :char,
|
36
|
-
pat: /^(?<v>[a-z]+)[01](\k<v>.)*(\s*\.+\s*)?(\k<v>.)+$/i,
|
37
|
-
names: ->(m) { [m[:v]] },
|
38
|
-
pat2: ->(_) { nil },
|
39
|
-
size: ->(f) { [f[-1]] }
|
40
|
-
},
|
41
|
-
{
|
42
|
-
container: :matrix,
|
43
|
-
item: :number,
|
44
|
-
pat: /^(?<v>[a-z]+)[01][01](\s+\k<v>..)*(\s+\.+)?(\s+\k<v>..)+$/i,
|
45
|
-
names: ->(m) { [m[:v]] },
|
46
|
-
pat2: ->(v) { /(^#{v}..(\s+#{v}..)*(\s+\.+)?(\s+#{v}..)+|\.+)$/ },
|
47
|
-
size: ->(f) { f[-2..-1].chars.to_a }
|
48
|
-
},
|
49
|
-
{
|
50
|
-
container: :matrix,
|
51
|
-
item: :char,
|
52
|
-
pat: /^(?<v>[a-z]+)[01][01](\k<v>..)*(\s*\.+\s*)?(\k<v>..)+$/i,
|
53
|
-
names: ->(m) { [m[:v]] },
|
54
|
-
pat2: ->(v) { /(^#{v}..(#{v}..)*(\s*\.+\s*)?(#{v}..)+|\.+)$/ },
|
55
|
-
size: ->(f) { f[-2..-1].chars.to_a }
|
56
|
-
},
|
57
|
-
{
|
58
|
-
container: :varray,
|
59
|
-
item: :number,
|
60
|
-
pat: /^[a-z]+(?<i>[0-9])(\s+[a-z]+\k<i>)*$/i,
|
61
|
-
names: ->(m) { m[0].split.map { |w| w[0..-2] } },
|
62
|
-
pat2: lambda { |vs|
|
63
|
-
pat = vs.map { |v| v + '.+' }.join('\s+')
|
64
|
-
/^(#{pat}|\.+)$/
|
65
|
-
},
|
66
|
-
size: ->(f) { /(?<sz>\d+)$/ =~ f ? [sz] : [f[-1]] }
|
67
|
-
},
|
68
|
-
{
|
69
|
-
container: :single,
|
70
|
-
item: :number,
|
71
|
-
pat: /^[a-z]+(\s+[a-z]+)*$/i,
|
72
|
-
names: ->(m) { m[0].split },
|
73
|
-
pat2: ->(_) { nil },
|
74
|
-
size: ->(_) { [] }
|
75
|
-
}
|
76
|
-
].freeze
|
77
|
-
|
78
|
-
def process(pbm)
|
79
|
-
defs = parse(pbm.fmt, pbm.smps)
|
80
|
-
pbm.defs = defs
|
81
|
-
end
|
82
|
-
|
83
|
-
def parse(fmt, smps)
|
84
|
-
lines = split_trim(fmt)
|
85
|
-
defs = parse_fmt(lines)
|
86
|
-
smpx = max_smp(smps)
|
87
|
-
return defs unless smpx
|
88
|
-
|
89
|
-
match_smp!(defs, smpx)
|
90
|
-
end
|
91
|
-
|
92
|
-
def split_trim(fmt)
|
93
|
-
fmt
|
94
|
-
.gsub(/[+-]1/, '') # N-1, N+1 -> N
|
95
|
-
.gsub(%r{[-/ ]}, ' ') # a-b, a/b -> a b
|
96
|
-
.gsub(/\{.*?\}/) { |w| w.delete(' ') } # {1, 1} -> {1,1} shortest match
|
97
|
-
.gsub(/[_,'\\(){}]/, '')
|
98
|
-
.gsub(/[::…‥]+/, '..')
|
99
|
-
.gsub(/ldots/, '..')
|
100
|
-
.gsub(/^[.\s]+$/, '..')
|
101
|
-
.split("\n")
|
102
|
-
.map(&:strip)
|
103
|
-
end
|
104
|
-
|
105
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
106
|
-
def parse_fmt(lines)
|
107
|
-
it = Iterator.new(lines + ['']) # sentinel
|
108
|
-
prv = nil
|
109
|
-
cur = it.next
|
110
|
-
Enumerator.new do |y|
|
111
|
-
loop do
|
112
|
-
unless (parser = PARSERS.find { |ps| ps[:pat] =~ cur })
|
113
|
-
puts "unknown format: #{cur}" unless cur.empty?
|
114
|
-
(cur = it.next) ? next : break
|
115
|
-
end
|
116
|
-
container, item = parser.values_at(:container, :item)
|
117
|
-
m = parser[:pat].match(cur)
|
118
|
-
names = parser[:names].call(m)
|
119
|
-
pat2 = parser[:pat2].call(names)
|
120
|
-
loop do
|
121
|
-
prv = cur
|
122
|
-
cur = it.next
|
123
|
-
break unless pat2 && pat2 =~ cur
|
124
|
-
end
|
125
|
-
size = parser[:size].call(prv)
|
126
|
-
y << InputDef.new(container, item, names, size)
|
127
|
-
end
|
128
|
-
end.to_a
|
129
|
-
end
|
130
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
131
|
-
|
132
|
-
def max_smp(smps)
|
133
|
-
smps
|
134
|
-
.select { |smp| smp.ext == :in }
|
135
|
-
.max_by { |smp| smp.txt.size }
|
136
|
-
&.txt
|
137
|
-
end
|
138
|
-
|
139
|
-
def match_smp!(inpdefs, smp)
|
140
|
-
lines = smp.split("\n")
|
141
|
-
inpdefs.each_with_index do |inpdef, i|
|
142
|
-
break if i > lines.size
|
143
|
-
next if inpdef.item != :number
|
144
|
-
|
145
|
-
inpdef.item = :string if lines[i].split[0] =~ /[^\-0-9]/
|
146
|
-
break if %i[varray matrix].include?(inpdef.container)
|
147
|
-
end
|
148
|
-
inpdefs
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AtCoderFriends
|
4
|
-
# run test cases for the specified program with actual input/output.
|
5
|
-
class JudgeTestRunner < TestRunner
|
6
|
-
include PathUtil
|
7
|
-
|
8
|
-
def initialize(ctx)
|
9
|
-
super(ctx)
|
10
|
-
@cases_dir = cases_dir(@dir)
|
11
|
-
@smp_dir = smp_dir(@dir)
|
12
|
-
end
|
13
|
-
|
14
|
-
def judge_all
|
15
|
-
puts "***** judge_all #{@prg} *****"
|
16
|
-
Dir["#{@cases_dir}/#{@q}/in/*.txt"].sort.each do |infile|
|
17
|
-
id = File.basename(infile, '.txt')
|
18
|
-
judge(id)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def judge_one(id)
|
23
|
-
puts "***** judge_one #{@prg} *****"
|
24
|
-
judge(id)
|
25
|
-
end
|
26
|
-
|
27
|
-
def judge(id)
|
28
|
-
infile = "#{@cases_dir}/#{@q}/in/#{id}.txt"
|
29
|
-
outfile = "#{@smp_dir}/#{@q}_#{id}.out"
|
30
|
-
expfile = "#{@cases_dir}/#{@q}/out/#{id}.txt"
|
31
|
-
run_test(id, infile, outfile, expfile)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AtCoderFriends
|
4
|
-
# run test cases for the specified program with sample input/output.
|
5
|
-
class SampleTestRunner < TestRunner
|
6
|
-
include PathUtil
|
7
|
-
|
8
|
-
def initialize(ctx)
|
9
|
-
super(ctx)
|
10
|
-
@smp_dir = smp_dir(@dir)
|
11
|
-
end
|
12
|
-
|
13
|
-
def test_all
|
14
|
-
puts "***** test_all #{@prg} *****"
|
15
|
-
1.upto(999) do |i|
|
16
|
-
break unless test(i)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def test_one(n)
|
21
|
-
puts "***** test_one #{@prg} *****"
|
22
|
-
test(n)
|
23
|
-
end
|
24
|
-
|
25
|
-
def test(n)
|
26
|
-
id = format('%<q>s_%<n>03d', q: @q, n: n)
|
27
|
-
files = %w[in out exp].map { |ext| "#{@smp_dir}/#{id}.#{ext}" }
|
28
|
-
run_test(id, *files)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,265 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'uri'
|
4
|
-
require 'cgi'
|
5
|
-
require 'json'
|
6
|
-
require 'mechanize'
|
7
|
-
require 'logger'
|
8
|
-
require 'English'
|
9
|
-
require 'io/console'
|
10
|
-
|
11
|
-
module AtCoderFriends
|
12
|
-
# scrapes AtCoder contest site and
|
13
|
-
# - fetches problems
|
14
|
-
# - submits sources
|
15
|
-
# - runs tests on custom_test page
|
16
|
-
class ScrapingAgent
|
17
|
-
include PathUtil
|
18
|
-
BASE_URL = 'https://atcoder.jp/'
|
19
|
-
XPATH_SECTION = '//h3[.="%<title>s"]/following-sibling::section'
|
20
|
-
XPATH_USERNAME = '//*[@id="navbar-collapse"]/ul[2]/li[2]/a'
|
21
|
-
SESSION_STORE =
|
22
|
-
File.join(Dir.home, '.at_coder_friends', '%<user>s_session.yml')
|
23
|
-
|
24
|
-
attr_reader :ctx, :agent
|
25
|
-
|
26
|
-
def initialize(ctx)
|
27
|
-
@ctx = ctx
|
28
|
-
@agent = Mechanize.new
|
29
|
-
agent.pre_connect_hooks << proc { sleep 0.1 }
|
30
|
-
agent.log = Logger.new(STDERR) if ctx.options[:debug]
|
31
|
-
agent.cookie_jar.load(session_store) if File.exist?(session_store)
|
32
|
-
end
|
33
|
-
|
34
|
-
def save_session
|
35
|
-
dir = File.dirname(session_store)
|
36
|
-
Dir.mkdir(dir) unless Dir.exist?(dir)
|
37
|
-
agent.cookie_jar.save_as(session_store)
|
38
|
-
end
|
39
|
-
|
40
|
-
def contest
|
41
|
-
@contest ||= contest_name(ctx.path)
|
42
|
-
end
|
43
|
-
|
44
|
-
def config
|
45
|
-
ctx.config
|
46
|
-
end
|
47
|
-
|
48
|
-
def common_url(path)
|
49
|
-
File.join(BASE_URL, path)
|
50
|
-
end
|
51
|
-
|
52
|
-
def contest_url(path = '')
|
53
|
-
File.join(BASE_URL, 'contests', contest, path)
|
54
|
-
end
|
55
|
-
|
56
|
-
def session_store
|
57
|
-
@session_store ||= format(SESSION_STORE, user: config['user'])
|
58
|
-
end
|
59
|
-
|
60
|
-
def constraints_pat
|
61
|
-
config['constraints_pat'] || '^制約$'
|
62
|
-
end
|
63
|
-
|
64
|
-
def input_fmt_pat
|
65
|
-
config['input_fmt_pat'] || '^入出?力$'
|
66
|
-
end
|
67
|
-
|
68
|
-
def input_smp_pat
|
69
|
-
config['input_smp_pat'] || '^入力例\s*(?<no>[\d0-9]+)$'
|
70
|
-
end
|
71
|
-
|
72
|
-
def output_smp_pat
|
73
|
-
config['output_smp_pat'] || '^出力例\s*(?<no>[\d0-9]+)$'
|
74
|
-
end
|
75
|
-
|
76
|
-
def fetch_with_auth(url)
|
77
|
-
begin
|
78
|
-
page = agent.get(url)
|
79
|
-
rescue Mechanize::ResponseCodeError => e
|
80
|
-
raise e unless e.response_code == '404'
|
81
|
-
raise e if username_link(e.page)
|
82
|
-
|
83
|
-
page = agent.get(common_url('login') + '?continue=' + CGI.escape(url))
|
84
|
-
end
|
85
|
-
|
86
|
-
if page.uri.path == '/login'
|
87
|
-
user, pass = read_auth
|
88
|
-
form = page.forms[1]
|
89
|
-
form.field_with(name: 'username').value = user
|
90
|
-
form.field_with(name: 'password').value = pass
|
91
|
-
page = form.submit
|
92
|
-
end
|
93
|
-
|
94
|
-
page.uri.path == '/login' && (raise AppError, 'Authentication failed.')
|
95
|
-
show_username(page)
|
96
|
-
page
|
97
|
-
end
|
98
|
-
|
99
|
-
def read_auth
|
100
|
-
user = config['user'].to_s
|
101
|
-
if user.empty?
|
102
|
-
print('Enter username:')
|
103
|
-
user = STDIN.gets.chomp
|
104
|
-
end
|
105
|
-
pass = config['password'].to_s
|
106
|
-
if pass.empty?
|
107
|
-
print("Enter password for #{user}:")
|
108
|
-
pass = STDIN.noecho(&:gets).chomp
|
109
|
-
puts
|
110
|
-
end
|
111
|
-
[user, pass]
|
112
|
-
end
|
113
|
-
|
114
|
-
def show_username(page)
|
115
|
-
username_old = @username
|
116
|
-
link = username_link(page)
|
117
|
-
@username = (link ? link.text.strip : '-')
|
118
|
-
return if @username == username_old || @username == '-'
|
119
|
-
|
120
|
-
puts "Logged in as #{@username}"
|
121
|
-
end
|
122
|
-
|
123
|
-
def username_link(page)
|
124
|
-
link = page.search(XPATH_USERNAME)[0]
|
125
|
-
link && link[:href] == '#' && link
|
126
|
-
end
|
127
|
-
|
128
|
-
def fetch_all
|
129
|
-
puts "***** fetch_all #{contest} *****"
|
130
|
-
fetch_assignments.map do |q, url|
|
131
|
-
pbm = fetch_problem(q, url)
|
132
|
-
yield pbm if block_given?
|
133
|
-
pbm
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def fetch_assignments
|
138
|
-
url = contest_url('tasks')
|
139
|
-
puts "fetch list from #{url} ..."
|
140
|
-
page = fetch_with_auth(url)
|
141
|
-
page
|
142
|
-
.search('//table[1]//td[1]//a')
|
143
|
-
.each_with_object({}) do |a, h|
|
144
|
-
h[a.text] = a[:href]
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def fetch_problem(q, url)
|
149
|
-
puts "fetch problem from #{url} ..."
|
150
|
-
page = fetch_with_auth(url)
|
151
|
-
Problem.new(q) do |pbm|
|
152
|
-
pbm.html = page.body
|
153
|
-
if contest == 'arc001'
|
154
|
-
page.search('//h3').each do |h3|
|
155
|
-
query = format(XPATH_SECTION, title: h3.content)
|
156
|
-
sections = page.search(query)
|
157
|
-
sections[0] && parse_section(pbm, h3, sections[0])
|
158
|
-
end
|
159
|
-
else
|
160
|
-
page.search('//*[./h3]').each do |section|
|
161
|
-
h3 = section.search('h3')[0]
|
162
|
-
parse_section(pbm, h3, section)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
def parse_section(pbm, h3, section)
|
169
|
-
title = h3.content.strip
|
170
|
-
title.delete!("\u008f\u0090") # agc002
|
171
|
-
text = section.content
|
172
|
-
code = section.search('pre')[0]&.content || ''
|
173
|
-
case title
|
174
|
-
when /#{constraints_pat}/
|
175
|
-
pbm.desc += text
|
176
|
-
when /#{input_fmt_pat}/
|
177
|
-
pbm.desc += text
|
178
|
-
pbm.fmt = code
|
179
|
-
when /#{input_smp_pat}/
|
180
|
-
pbm.add_smp($LAST_MATCH_INFO[:no], :in, code)
|
181
|
-
when /#{output_smp_pat}/
|
182
|
-
pbm.add_smp($LAST_MATCH_INFO[:no], :exp, code)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def submit
|
187
|
-
path, _dir, prg, _base, ext, q = split_prg_path(ctx.path)
|
188
|
-
puts "***** submit #{prg} *****"
|
189
|
-
src = File.read(path, encoding: Encoding::UTF_8)
|
190
|
-
|
191
|
-
page = fetch_with_auth(contest_url('submit'))
|
192
|
-
form = page.forms[1]
|
193
|
-
form.field_with(name: 'data.TaskScreenName') do |sel|
|
194
|
-
option = sel.options.find { |op| op.text.start_with?(q) }
|
195
|
-
option&.select || (raise AppError, "unknown problem:#{q}.")
|
196
|
-
end
|
197
|
-
form.add_field!('data.LanguageId', lang_id(ext))
|
198
|
-
form.field_with(name: 'sourceCode').value = src
|
199
|
-
form.submit
|
200
|
-
end
|
201
|
-
|
202
|
-
def code_test(infile)
|
203
|
-
path, _dir, _prg, _base, ext, _q = split_prg_path(ctx.path)
|
204
|
-
src = File.read(path, encoding: Encoding::UTF_8)
|
205
|
-
data = File.read(infile)
|
206
|
-
|
207
|
-
page = fetch_with_auth(contest_url('custom_test'))
|
208
|
-
script = page.search('script').text
|
209
|
-
csrf_token = script.scan(/var csrfToken = "(.*)"/)[0][0]
|
210
|
-
|
211
|
-
page = agent.post(
|
212
|
-
contest_url('custom_test/submit/json'),
|
213
|
-
'data.LanguageId' => lang_id(ext),
|
214
|
-
'sourceCode' => src,
|
215
|
-
'input' => data,
|
216
|
-
'csrf_token' => csrf_token
|
217
|
-
)
|
218
|
-
msg = page.body
|
219
|
-
raise AppError, msg unless msg.empty?
|
220
|
-
|
221
|
-
100.times do
|
222
|
-
page = agent.get(contest_url('custom_test/json?reload=true'))
|
223
|
-
data = JSON.parse(page.body)
|
224
|
-
return nil unless data.is_a?(Hash) && data['Result']
|
225
|
-
return data if data.dig('Result', 'Status') == 3
|
226
|
-
return data unless data['Interval']
|
227
|
-
|
228
|
-
sleep 1.0 * data['Interval'] / 1000
|
229
|
-
end
|
230
|
-
|
231
|
-
nil
|
232
|
-
end
|
233
|
-
|
234
|
-
def lang_list
|
235
|
-
@lang_list ||= begin
|
236
|
-
page = fetch_with_auth(contest_url('custom_test'))
|
237
|
-
form = page.forms[1]
|
238
|
-
sel = form.field_with(name: 'data.LanguageId')
|
239
|
-
sel && sel
|
240
|
-
.options
|
241
|
-
.reject { |opt| opt.value.empty? }
|
242
|
-
.map do |opt|
|
243
|
-
{ v: opt.value, t: opt.text }
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def lang_list_txt
|
249
|
-
lang_list
|
250
|
-
&.map { |opt| "#{opt[:v]} - #{opt[:t]}" }
|
251
|
-
&.join("\n")
|
252
|
-
end
|
253
|
-
|
254
|
-
def lang_id(ext)
|
255
|
-
config.dig('ext_settings', ext, 'submit_lang') || (
|
256
|
-
msg = <<~MSG
|
257
|
-
submit_lang for .#{ext} is not specified.
|
258
|
-
Available languages:
|
259
|
-
#{lang_list_txt || '(failed to fetch)'}
|
260
|
-
MSG
|
261
|
-
raise AppError, msg
|
262
|
-
)
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|