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.
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