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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -2
  3. data/.rubocop.yml +1 -0
  4. data/.rubocop_todo.yml +4 -1
  5. data/Gemfile.lock +16 -14
  6. data/Rakefile +2 -0
  7. data/at_coder_friends.gemspec +1 -0
  8. data/lib/at_coder_friends.rb +13 -5
  9. data/lib/at_coder_friends/cli.rb +1 -2
  10. data/lib/at_coder_friends/config_loader.rb +2 -2
  11. data/lib/at_coder_friends/context.rb +3 -3
  12. data/lib/at_coder_friends/cxx_generator.rb +7 -13
  13. data/lib/at_coder_friends/emitter.rb +0 -4
  14. data/lib/at_coder_friends/parser/constraints_parser.rb +26 -0
  15. data/lib/at_coder_friends/parser/format_parser.rb +154 -0
  16. data/lib/at_coder_friends/parser/main.rb +16 -0
  17. data/lib/at_coder_friends/parser/page_parser.rb +119 -0
  18. data/lib/at_coder_friends/path_util.rb +10 -0
  19. data/lib/at_coder_friends/problem.rb +11 -14
  20. data/lib/at_coder_friends/scraping/agent.rb +77 -0
  21. data/lib/at_coder_friends/scraping/authentication.rb +71 -0
  22. data/lib/at_coder_friends/scraping/custom_test.rb +53 -0
  23. data/lib/at_coder_friends/scraping/session.rb +26 -0
  24. data/lib/at_coder_friends/scraping/submission.rb +31 -0
  25. data/lib/at_coder_friends/scraping/tasks.rb +39 -0
  26. data/lib/at_coder_friends/test_runner/base.rb +123 -0
  27. data/lib/at_coder_friends/test_runner/judge.rb +44 -0
  28. data/lib/at_coder_friends/test_runner/sample.rb +35 -0
  29. data/lib/at_coder_friends/verifier.rb +4 -2
  30. data/lib/at_coder_friends/version.rb +1 -1
  31. data/tasks/regression.rake +163 -0
  32. metadata +30 -7
  33. data/lib/at_coder_friends/format_parser.rb +0 -151
  34. data/lib/at_coder_friends/judge_test_runner.rb +0 -34
  35. data/lib/at_coder_friends/sample_test_runner.rb +0 -31
  36. data/lib/at_coder_friends/scraping_agent.rb +0 -265
  37. 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