at_coder_friends 0.5.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
module Parser
|
5
|
+
# parses problem page and collect problem information
|
6
|
+
module PageParser
|
7
|
+
module_function
|
8
|
+
|
9
|
+
SECTION_TYPES = [
|
10
|
+
{
|
11
|
+
key: 'constraints',
|
12
|
+
patterns: [
|
13
|
+
'^制約$',
|
14
|
+
'^Constraints$'
|
15
|
+
]
|
16
|
+
},
|
17
|
+
{
|
18
|
+
key: 'input format',
|
19
|
+
patterns: [
|
20
|
+
'^入出?力(形式)?$',
|
21
|
+
'^Inputs?\s*(,|and)?\s*(Outputs?)?\s*(Format)?$'
|
22
|
+
]
|
23
|
+
},
|
24
|
+
{
|
25
|
+
key: 'sample input %<no>s',
|
26
|
+
patterns: [
|
27
|
+
'^入力例\s*(?<no>\d+)?$',
|
28
|
+
'^入力\s*(?<no>\d+)$',
|
29
|
+
'^Sample\s*Input\s*(?<no>\d+)?$',
|
30
|
+
'^Input\s*Example\s*(?<no>\d+)?$',
|
31
|
+
'^Input\s*(?<no>\d+)$'
|
32
|
+
]
|
33
|
+
},
|
34
|
+
{
|
35
|
+
key: 'sample output %<no>s',
|
36
|
+
patterns: [
|
37
|
+
'^出力例\s*(?<no>\d+)?$',
|
38
|
+
'^出力\s*(?<no>\d+)$',
|
39
|
+
'^入力例\s*(?<no>\d+)?\s*に対する出力例$',
|
40
|
+
'^Sample\s*Output\s*(?<no>\d+)?$',
|
41
|
+
'^Output\s*Example\s*(?<no>\d+)?$',
|
42
|
+
'^Output\s*(?<no>\d+)$',
|
43
|
+
'^Output\s*for\s*(the)?\s*Sample\s*Input\s*(?<no>\d+)?$'
|
44
|
+
]
|
45
|
+
}
|
46
|
+
].freeze
|
47
|
+
|
48
|
+
def process(pbm)
|
49
|
+
sections = collect_sections(pbm.page)
|
50
|
+
apply_sections(pbm, sections)
|
51
|
+
end
|
52
|
+
|
53
|
+
def collect_sections(page)
|
54
|
+
sections = {}
|
55
|
+
%w[h2 h3].each do |tag|
|
56
|
+
page
|
57
|
+
.search(tag)
|
58
|
+
.each do |h|
|
59
|
+
key = find_key(h)
|
60
|
+
key && sections[key] ||= parse_section(h)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
sections
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_key(h)
|
67
|
+
title = normalize(h.content)
|
68
|
+
SECTION_TYPES.each do |grp|
|
69
|
+
grp[:patterns].each do |pat|
|
70
|
+
m = title.match(/#{pat}/i)
|
71
|
+
next unless m
|
72
|
+
|
73
|
+
no = m.names.include?('no') && m['no'] || '1'
|
74
|
+
return format(grp[:key], no: no)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_section(h)
|
81
|
+
text = ''
|
82
|
+
pre = nil
|
83
|
+
nx = h.next
|
84
|
+
while nx && nx.name != h.name
|
85
|
+
text += nx.content.gsub("\r\n", "\n")
|
86
|
+
%w[pre blockquote].each do |tag|
|
87
|
+
pre ||= (nx.name == tag ? nx : nx.search(tag)[0])
|
88
|
+
end
|
89
|
+
nx = nx.next
|
90
|
+
end
|
91
|
+
code = (pre&.text || '').lstrip.gsub("\r\n", "\n")
|
92
|
+
[text, code]
|
93
|
+
end
|
94
|
+
|
95
|
+
def apply_sections(pbm, sections)
|
96
|
+
sections.each do |key, (text, code)|
|
97
|
+
case key
|
98
|
+
when 'constraints'
|
99
|
+
pbm.desc += text
|
100
|
+
when 'input format'
|
101
|
+
pbm.desc += text
|
102
|
+
pbm.fmt = code
|
103
|
+
when /^sample input (?<no>\d+)$/
|
104
|
+
pbm.add_smp($LAST_MATCH_INFO[:no], :in, code)
|
105
|
+
when /^sample output (?<no>\d+)$/
|
106
|
+
pbm.add_smp($LAST_MATCH_INFO[:no], :exp, code)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def normalize(s)
|
112
|
+
s
|
113
|
+
.tr(' 0-9A-Za-z', ' 0-9A-Za-z')
|
114
|
+
.gsub(/[^一-龠_ぁ-ん_ァ-ヶーa-zA-Z0-9 ]/, '')
|
115
|
+
.strip
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -7,6 +7,7 @@ module AtCoderFriends
|
|
7
7
|
|
8
8
|
SMP_DIR = 'data'
|
9
9
|
CASES_DIR = 'cases'
|
10
|
+
TMP_DIR = '.tmp'
|
10
11
|
|
11
12
|
def contest_name(path)
|
12
13
|
dir = File.file?(path) ? File.dirname(path) : path
|
@@ -27,5 +28,14 @@ module AtCoderFriends
|
|
27
28
|
def cases_dir(dir)
|
28
29
|
File.join(dir, CASES_DIR)
|
29
30
|
end
|
31
|
+
|
32
|
+
def tmp_dir(path)
|
33
|
+
dir = File.dirname(path)
|
34
|
+
File.join(dir, '.tmp')
|
35
|
+
end
|
36
|
+
|
37
|
+
def makedirs_unless(dir)
|
38
|
+
FileUtils.makedirs(dir) unless Dir.exist?(dir)
|
39
|
+
end
|
30
40
|
end
|
31
41
|
end
|
@@ -1,11 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AtCoderFriends
|
4
|
-
|
4
|
+
SampleData = Struct.new(:no, :ext, :txt) do
|
5
5
|
def initialize(no, ext, txt)
|
6
|
-
no
|
7
|
-
txt = txt.lstrip.gsub("\r\n", "\n")
|
8
|
-
super(no, ext, txt)
|
6
|
+
super(no.to_i, ext, txt)
|
9
7
|
end
|
10
8
|
end
|
11
9
|
|
@@ -15,34 +13,33 @@ module AtCoderFriends
|
|
15
13
|
end
|
16
14
|
end
|
17
15
|
|
18
|
-
|
16
|
+
Constraint = Struct.new(:name, :type, :value)
|
17
|
+
|
18
|
+
SourceCode = Struct.new(:ext, :txt)
|
19
19
|
|
20
20
|
# holds problem information
|
21
21
|
class Problem
|
22
|
-
attr_reader :q, :
|
23
|
-
attr_accessor :
|
22
|
+
attr_reader :q, :smps, :srcs
|
23
|
+
attr_accessor :page, :desc, :fmt, :defs, :constraints
|
24
24
|
|
25
25
|
def initialize(q)
|
26
26
|
@q = q
|
27
|
-
@
|
27
|
+
@page = nil
|
28
28
|
@desc = ''
|
29
29
|
@fmt = ''
|
30
30
|
@smps = []
|
31
31
|
@defs = []
|
32
|
+
@constraints = []
|
32
33
|
@srcs = []
|
33
34
|
yield self if block_given?
|
34
35
|
end
|
35
36
|
|
36
|
-
def fmt=(f)
|
37
|
-
@fmt = f.lstrip.gsub("\r\n", "\n")
|
38
|
-
end
|
39
|
-
|
40
37
|
def add_smp(no, ext, txt)
|
41
|
-
@smps <<
|
38
|
+
@smps << SampleData.new(no, ext, txt)
|
42
39
|
end
|
43
40
|
|
44
41
|
def add_src(ext, txt)
|
45
|
-
@srcs <<
|
42
|
+
@srcs << SourceCode.new(ext, txt)
|
46
43
|
end
|
47
44
|
end
|
48
45
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mechanize'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module AtCoderFriends
|
7
|
+
module Scraping
|
8
|
+
# common functions for scraping
|
9
|
+
class Agent
|
10
|
+
include AtCoderFriends::PathUtil
|
11
|
+
include Session
|
12
|
+
include Authentication
|
13
|
+
include Tasks
|
14
|
+
include CustomTest
|
15
|
+
include Submission
|
16
|
+
|
17
|
+
BASE_URL = 'https://atcoder.jp/'
|
18
|
+
|
19
|
+
attr_reader :ctx, :agent
|
20
|
+
|
21
|
+
def initialize(ctx)
|
22
|
+
@ctx = ctx
|
23
|
+
@agent = Mechanize.new
|
24
|
+
agent.pre_connect_hooks << proc { sleep 0.1 }
|
25
|
+
agent.log = Logger.new(STDERR) if ctx.options[:debug]
|
26
|
+
load_session
|
27
|
+
end
|
28
|
+
|
29
|
+
def contest
|
30
|
+
@contest ||= contest_name(ctx.path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
ctx.config
|
35
|
+
end
|
36
|
+
|
37
|
+
def common_url(path)
|
38
|
+
File.join(BASE_URL, path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def contest_url(path = '')
|
42
|
+
File.join(BASE_URL, 'contests', contest, path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def lang_id(ext)
|
46
|
+
config.dig('ext_settings', ext, 'submit_lang') || (
|
47
|
+
msg = <<~MSG
|
48
|
+
submit_lang for .#{ext} is not specified.
|
49
|
+
Available languages:
|
50
|
+
#{lang_list_txt || '(failed to fetch)'}
|
51
|
+
MSG
|
52
|
+
raise AppError, msg
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def lang_list_txt
|
57
|
+
lang_list
|
58
|
+
&.map { |opt| "#{opt[:v]} - #{opt[:t]}" }
|
59
|
+
&.join("\n")
|
60
|
+
end
|
61
|
+
|
62
|
+
def lang_list
|
63
|
+
@lang_list ||= begin
|
64
|
+
page = fetch_with_auth(contest_url('custom_test'))
|
65
|
+
form = page.forms[1]
|
66
|
+
sel = form.field_with(name: 'data.LanguageId')
|
67
|
+
sel && sel
|
68
|
+
.options
|
69
|
+
.reject { |opt| opt.value.empty? }
|
70
|
+
.map do |opt|
|
71
|
+
{ v: opt.value, t: opt.text }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'io/console'
|
5
|
+
|
6
|
+
module AtCoderFriends
|
7
|
+
module Scraping
|
8
|
+
# fetch pages and
|
9
|
+
# authenticates user at login page if needed
|
10
|
+
module Authentication
|
11
|
+
XPATH_USERNAME = '//*[@id="navbar-collapse"]/ul[2]/li[2]/a'
|
12
|
+
|
13
|
+
def fetch_with_auth(url)
|
14
|
+
page = fetch_raw(url)
|
15
|
+
page.uri.path == '/login' && page = post_login(page)
|
16
|
+
page.uri.path == '/login' && (raise AppError, 'Authentication failed.')
|
17
|
+
show_username(page)
|
18
|
+
page
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_raw(url)
|
22
|
+
begin
|
23
|
+
return agent.get(url)
|
24
|
+
rescue Mechanize::ResponseCodeError => e
|
25
|
+
raise e unless e.response_code == '404'
|
26
|
+
raise e if username_link(e.page)
|
27
|
+
end
|
28
|
+
|
29
|
+
agent.get(common_url('login') + '?continue=' + CGI.escape(url))
|
30
|
+
end
|
31
|
+
|
32
|
+
def post_login(page)
|
33
|
+
user, pass = read_auth
|
34
|
+
form = page.forms[1]
|
35
|
+
form.field_with(name: 'username').value = user
|
36
|
+
form.field_with(name: 'password').value = pass
|
37
|
+
form.submit
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_auth
|
41
|
+
user = config['user'].to_s
|
42
|
+
if user.empty?
|
43
|
+
print('Enter username:')
|
44
|
+
user = STDIN.gets.chomp
|
45
|
+
end
|
46
|
+
|
47
|
+
pass = config['password'].to_s
|
48
|
+
if pass.empty?
|
49
|
+
print("Enter password for #{user}:")
|
50
|
+
pass = STDIN.noecho(&:gets).chomp
|
51
|
+
puts
|
52
|
+
end
|
53
|
+
[user, pass]
|
54
|
+
end
|
55
|
+
|
56
|
+
def show_username(page)
|
57
|
+
username_bak = @username
|
58
|
+
link = username_link(page)
|
59
|
+
@username = (link ? link.text.strip : '-')
|
60
|
+
return if @username == username_bak || @username == '-'
|
61
|
+
|
62
|
+
puts "Logged in as #{@username}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def username_link(page)
|
66
|
+
link = page.search(XPATH_USERNAME)[0]
|
67
|
+
link && link[:href] == '#' && link
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module AtCoderFriends
|
6
|
+
module Scraping
|
7
|
+
# run tests on custom_test page
|
8
|
+
module CustomTest
|
9
|
+
include AtCoderFriends::PathUtil
|
10
|
+
|
11
|
+
def code_test(infile)
|
12
|
+
path, _dir, _prg, _base, ext, _q = split_prg_path(ctx.path)
|
13
|
+
lang = lang_id(ext)
|
14
|
+
src = File.read(path, encoding: Encoding::UTF_8)
|
15
|
+
data = File.read(infile)
|
16
|
+
|
17
|
+
post_custom_test(lang, src, data)
|
18
|
+
check_custom_test
|
19
|
+
end
|
20
|
+
|
21
|
+
def post_custom_test(lang, src, data)
|
22
|
+
page = fetch_with_auth(contest_url('custom_test'))
|
23
|
+
script = page.search('script').text
|
24
|
+
csrf_token = script.scan(/var csrfToken = "(.*)"/)[0][0]
|
25
|
+
|
26
|
+
page = agent.post(
|
27
|
+
contest_url('custom_test/submit/json'),
|
28
|
+
'data.LanguageId' => lang,
|
29
|
+
'sourceCode' => src,
|
30
|
+
'input' => data,
|
31
|
+
'csrf_token' => csrf_token
|
32
|
+
)
|
33
|
+
|
34
|
+
msg = page.body
|
35
|
+
raise AppError, msg unless msg.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_custom_test
|
39
|
+
100.times do
|
40
|
+
page = agent.get(contest_url('custom_test/json?reload=true'))
|
41
|
+
data = JSON.parse(page.body)
|
42
|
+
return nil unless data.is_a?(Hash) && data['Result']
|
43
|
+
return data if data.dig('Result', 'Status') == 3
|
44
|
+
return data unless data['Interval']
|
45
|
+
|
46
|
+
sleep 1.0 * data['Interval'] / 1000
|
47
|
+
end
|
48
|
+
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
module Scraping
|
5
|
+
# session management for scraping
|
6
|
+
module Session
|
7
|
+
SESSION_STORE_FMT = File.join(
|
8
|
+
Dir.home, '.at_coder_friends', '%<user>s_session.yml'
|
9
|
+
)
|
10
|
+
|
11
|
+
def load_session
|
12
|
+
agent.cookie_jar.load(session_store) if File.exist?(session_store)
|
13
|
+
end
|
14
|
+
|
15
|
+
def save_session
|
16
|
+
dir = File.dirname(session_store)
|
17
|
+
Dir.mkdir(dir) unless Dir.exist?(dir)
|
18
|
+
agent.cookie_jar.save_as(session_store)
|
19
|
+
end
|
20
|
+
|
21
|
+
def session_store
|
22
|
+
@session_store ||= format(SESSION_STORE_FMT, user: config['user'])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
module Scraping
|
5
|
+
# submit sources on submit page
|
6
|
+
module Submission
|
7
|
+
include AtCoderFriends::PathUtil
|
8
|
+
|
9
|
+
def submit
|
10
|
+
path, _dir, prg, _base, ext, q = split_prg_path(ctx.path)
|
11
|
+
puts "***** submit #{prg} *****"
|
12
|
+
lang = lang_id(ext)
|
13
|
+
src = File.read(path, encoding: Encoding::UTF_8)
|
14
|
+
|
15
|
+
post_submit(q, lang, src)
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_submit(q, lang, src)
|
19
|
+
page = fetch_with_auth(contest_url('submit'))
|
20
|
+
form = page.forms[1]
|
21
|
+
form.field_with(name: 'data.TaskScreenName') do |sel|
|
22
|
+
option = sel.options.find { |op| op.text.start_with?(q) }
|
23
|
+
option&.select || (raise AppError, "unknown problem:#{q}.")
|
24
|
+
end
|
25
|
+
form.add_field!('data.LanguageId', lang)
|
26
|
+
form.field_with(name: 'sourceCode').value = src
|
27
|
+
form.submit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|