at_coder_friends 0.5.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +1 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/config/default.yml +3 -0
- data/docs/CONFIGURATION.md +74 -9
- data/lib/at_coder_friends.rb +10 -5
- data/lib/at_coder_friends/cli.rb +2 -5
- data/lib/at_coder_friends/context.rb +4 -0
- data/lib/at_coder_friends/emitter.rb +2 -2
- data/lib/at_coder_friends/generator/cxx_builtin.rb +191 -0
- data/lib/at_coder_friends/generator/main.rb +53 -0
- data/lib/at_coder_friends/generator/ruby_builtin.rb +128 -0
- data/lib/at_coder_friends/parser/binary.rb +39 -0
- data/lib/at_coder_friends/parser/constraints.rb +36 -0
- data/lib/at_coder_friends/parser/{format_parser.rb → input_format.rb} +42 -30
- data/lib/at_coder_friends/parser/interactive.rb +29 -0
- data/lib/at_coder_friends/parser/main.rb +6 -3
- data/lib/at_coder_friends/parser/sample_data.rb +24 -0
- data/lib/at_coder_friends/parser/section_wrapper.rb +49 -0
- data/lib/at_coder_friends/parser/{page_parser.rb → sections.rb} +44 -50
- data/lib/at_coder_friends/problem.rb +40 -24
- data/lib/at_coder_friends/scraping/agent.rb +1 -5
- data/lib/at_coder_friends/scraping/authentication.rb +2 -2
- data/lib/at_coder_friends/scraping/session.rb +1 -1
- data/lib/at_coder_friends/scraping/tasks.rb +2 -6
- data/lib/at_coder_friends/test_runner/base.rb +36 -31
- data/lib/at_coder_friends/test_runner/judge.rb +2 -6
- data/lib/at_coder_friends/test_runner/sample.rb +8 -6
- data/lib/at_coder_friends/version.rb +1 -1
- data/tasks/regression/check_diff.rake +29 -0
- data/tasks/regression/check_parse.rake +56 -0
- data/tasks/regression/regression.rb +67 -0
- data/tasks/regression/section_list.rake +41 -0
- data/tasks/regression/setup.rake +48 -0
- data/templates/cxx_builtin_default.cxx +26 -0
- data/templates/cxx_builtin_interactive.cxx +61 -0
- data/templates/ruby_builtin_default.rb +5 -0
- data/templates/ruby_builtin_interactive.rb +32 -0
- metadata +21 -8
- data/lib/at_coder_friends/cxx_generator.rb +0 -169
- data/lib/at_coder_friends/parser/constraints_parser.rb +0 -26
- data/lib/at_coder_friends/ruby_generator.rb +0 -97
- data/tasks/regression.rake +0 -163
@@ -1,45 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AtCoderFriends
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
# holds problem information
|
5
|
+
class Problem
|
6
|
+
SECTION_IN_FMT = 'INPUT_FORMAT'
|
7
|
+
SECTION_OUT_FMT = 'OUTPUT_FORMAT'
|
8
|
+
SECTION_IO_FMT = 'INOUT_FORMAT'
|
9
|
+
SECTION_CONSTRAINTS = 'CONSTRAINTS'
|
10
|
+
SECTION_IN_SMP = 'INPUT_SAMPLE_%<no>s'
|
11
|
+
SECTION_IN_SMP_PAT = /^INPUT_SAMPLE_(?<no>\d+)$/.freeze
|
12
|
+
SECTION_OUT_SMP = 'OUTPUT_SAMPLE_%<no>s'
|
13
|
+
SECTION_OUT_SMP_PAT = /^OUTPUT_SAMPLE_(?<no>\d+)$/.freeze
|
14
|
+
SECTION_IO_SMP = 'INOUT_SAMPLE'
|
15
|
+
|
16
|
+
SampleData = Struct.new(:no, :ext, :txt)
|
17
|
+
|
18
|
+
InputFormat = Struct.new(:container, :item, :names, :size) do
|
19
|
+
def initialize(container, item, names, size = [])
|
20
|
+
super(container, item, names, size)
|
21
|
+
end
|
7
22
|
end
|
8
|
-
end
|
9
23
|
|
10
|
-
|
11
|
-
def initialize(container, item, names, size = [])
|
12
|
-
super(container, item, names, size)
|
13
|
-
end
|
14
|
-
end
|
24
|
+
Constraint = Struct.new(:name, :type, :value)
|
15
25
|
|
16
|
-
|
26
|
+
Options = Struct.new(:interactive, :binary_values)
|
17
27
|
|
18
|
-
|
28
|
+
SourceCode = Struct.new(:ext, :txt)
|
19
29
|
|
20
|
-
|
21
|
-
|
22
|
-
attr_reader :q, :smps, :srcs
|
23
|
-
attr_accessor :page, :desc, :fmt, :defs, :constraints
|
30
|
+
attr_reader :q, :samples, :sources, :options
|
31
|
+
attr_accessor :page, :sections, :formats, :constraints
|
24
32
|
|
25
|
-
def initialize(q)
|
33
|
+
def initialize(q, page = Mechanize::Page.new)
|
26
34
|
@q = q
|
27
|
-
@page =
|
28
|
-
@
|
29
|
-
@
|
30
|
-
@
|
31
|
-
@defs = []
|
35
|
+
@page = page
|
36
|
+
@sections = {}
|
37
|
+
@samples = []
|
38
|
+
@formats = []
|
32
39
|
@constraints = []
|
33
|
-
@
|
40
|
+
@options = Options.new
|
41
|
+
@sources = []
|
34
42
|
yield self if block_given?
|
35
43
|
end
|
36
44
|
|
45
|
+
def url
|
46
|
+
@url ||= page.uri.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def page_body
|
50
|
+
@page_body ||= page.body.force_encoding('utf-8')
|
51
|
+
end
|
52
|
+
|
37
53
|
def add_smp(no, ext, txt)
|
38
|
-
@
|
54
|
+
@samples << SampleData.new(no, ext, txt)
|
39
55
|
end
|
40
56
|
|
41
57
|
def add_src(ext, txt)
|
42
|
-
@
|
58
|
+
@sources << SourceCode.new(ext, txt)
|
43
59
|
end
|
44
60
|
end
|
45
61
|
end
|
@@ -30,10 +30,6 @@ module AtCoderFriends
|
|
30
30
|
@contest ||= contest_name(ctx.path)
|
31
31
|
end
|
32
32
|
|
33
|
-
def config
|
34
|
-
ctx.config
|
35
|
-
end
|
36
|
-
|
37
33
|
def common_url(path)
|
38
34
|
File.join(BASE_URL, path)
|
39
35
|
end
|
@@ -43,7 +39,7 @@ module AtCoderFriends
|
|
43
39
|
end
|
44
40
|
|
45
41
|
def lang_id(ext)
|
46
|
-
config.dig('ext_settings', ext, 'submit_lang') || (
|
42
|
+
ctx.config.dig('ext_settings', ext, 'submit_lang') || (
|
47
43
|
msg = <<~MSG
|
48
44
|
submit_lang for .#{ext} is not specified.
|
49
45
|
Available languages:
|
@@ -38,13 +38,13 @@ module AtCoderFriends
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def read_auth
|
41
|
-
user = config['user'].to_s
|
41
|
+
user = ctx.config['user'].to_s
|
42
42
|
if user.empty?
|
43
43
|
print('Enter username:')
|
44
44
|
user = STDIN.gets.chomp
|
45
45
|
end
|
46
46
|
|
47
|
-
pass = config['password'].to_s
|
47
|
+
pass = ctx.config['password'].to_s
|
48
48
|
if pass.empty?
|
49
49
|
print("Enter password for #{user}:")
|
50
50
|
pass = STDIN.noecho(&:gets).chomp
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'English'
|
4
|
-
|
5
3
|
module AtCoderFriends
|
6
4
|
module Scraping
|
7
5
|
# fetch problems from tasks page
|
@@ -29,10 +27,8 @@ module AtCoderFriends
|
|
29
27
|
def fetch_problem(q, url)
|
30
28
|
puts "fetch problem from #{url} ..."
|
31
29
|
page = fetch_with_auth(url)
|
32
|
-
|
33
|
-
|
34
|
-
pbm.page = page
|
35
|
-
end
|
30
|
+
page.search('br').each { |br| br.replace("\n") }
|
31
|
+
Problem.new(q, page)
|
36
32
|
end
|
37
33
|
end
|
38
34
|
end
|
@@ -8,25 +8,26 @@ module AtCoderFriends
|
|
8
8
|
# run tests for the specified program.
|
9
9
|
class Base
|
10
10
|
include PathUtil
|
11
|
+
STATUS_STR = {
|
12
|
+
OK: '<< OK >>'.green,
|
13
|
+
WA: '!!!!! WA !!!!!'.red,
|
14
|
+
RE: '!!!!! RE !!!!!'.red,
|
15
|
+
NO_EXP: ''
|
16
|
+
}.freeze
|
11
17
|
|
12
18
|
attr_reader :ctx, :path, :dir, :prg, :base, :ext, :q
|
13
19
|
|
14
20
|
def initialize(ctx)
|
15
21
|
@ctx = ctx
|
16
22
|
@path, @dir, @prg, @base, @ext, @q = split_prg_path(ctx.path)
|
17
|
-
|
18
|
-
|
19
|
-
def config
|
20
|
-
ctx.config
|
23
|
+
@detail = true
|
21
24
|
end
|
22
25
|
|
23
26
|
def test_cmd
|
24
27
|
@test_cmd ||= begin
|
25
|
-
cmds = config.dig('ext_settings', ext, 'test_cmd')
|
28
|
+
cmds = ctx.config.dig('ext_settings', ext, 'test_cmd')
|
26
29
|
cmd = cmds && (cmds[which_os.to_s] || cmds['default'])
|
27
|
-
|
28
|
-
|
29
|
-
cmd.gsub('{dir}', dir).gsub('{base}', base)
|
30
|
+
cmd&.gsub('{dir}', dir)&.gsub('{base}', base)
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
@@ -39,18 +40,25 @@ module AtCoderFriends
|
|
39
40
|
end
|
40
41
|
|
41
42
|
def run_test(id, infile, outfile, expfile)
|
42
|
-
return false unless File.exist?(infile) && File.exist?(expfile)
|
43
|
-
|
44
43
|
puts "==== #{id} ===="
|
44
|
+
return false unless check_file(infile, outfile)
|
45
45
|
|
46
|
-
makedirs_unless(File.dirname(outfile))
|
47
46
|
is_success = send(test_mtd, infile, outfile)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
input = File.read(infile)
|
48
|
+
result = File.read(outfile)
|
49
|
+
expected = File.exist?(expfile) && File.read(expfile)
|
50
|
+
status = check_status(is_success, result, expected)
|
51
|
+
print detail_str(input, result, expected) if @detail
|
52
|
+
puts STATUS_STR[status]
|
53
|
+
status == :OK
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_file(infile, outfile)
|
57
|
+
unless File.exist?(infile)
|
58
|
+
puts "#{File.basename(infile)} not found."
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
makedirs_unless(File.dirname(outfile))
|
54
62
|
true
|
55
63
|
end
|
56
64
|
|
@@ -76,9 +84,16 @@ module AtCoderFriends
|
|
76
84
|
[true, res['Stdout']]
|
77
85
|
end
|
78
86
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
87
|
+
def check_status(is_success, result, expected)
|
88
|
+
if !is_success
|
89
|
+
:RE
|
90
|
+
elsif !expected
|
91
|
+
:NO_EXP
|
92
|
+
elsif result != expected
|
93
|
+
:WA
|
94
|
+
else
|
95
|
+
:OK
|
96
|
+
end
|
82
97
|
end
|
83
98
|
|
84
99
|
def detail_str(input, result, expected)
|
@@ -86,22 +101,12 @@ module AtCoderFriends
|
|
86
101
|
ret += "-- input --\n"
|
87
102
|
ret += input
|
88
103
|
ret += "-- expected --\n"
|
89
|
-
ret += expected
|
104
|
+
ret += expected || "(no expected value)\n"
|
90
105
|
ret += "-- result --\n"
|
91
106
|
ret += result
|
92
107
|
ret
|
93
108
|
end
|
94
109
|
|
95
|
-
def result_str(is_success, result, expected)
|
96
|
-
if !is_success
|
97
|
-
'!!!!! RE !!!!!'.red
|
98
|
-
elsif result != expected
|
99
|
-
'!!!!! WA !!!!!'.red
|
100
|
-
else
|
101
|
-
'<< OK >>'.green
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
110
|
def which_os
|
106
111
|
@which_os ||= begin
|
107
112
|
case RbConfig::CONFIG['host_os']
|
@@ -16,10 +16,11 @@ module AtCoderFriends
|
|
16
16
|
|
17
17
|
def judge_all
|
18
18
|
puts "***** judge_all #{prg} (#{test_loc}) *****"
|
19
|
-
Dir["#{data_dir}/#{q}/in/*.txt"].sort.
|
19
|
+
results = Dir["#{data_dir}/#{q}/in/*.txt"].sort.map do |infile|
|
20
20
|
id = File.basename(infile, '.txt')
|
21
21
|
judge(id, false)
|
22
22
|
end
|
23
|
+
!results.empty? && results.all?
|
23
24
|
end
|
24
25
|
|
25
26
|
def judge_one(id)
|
@@ -34,11 +35,6 @@ module AtCoderFriends
|
|
34
35
|
expfile = "#{data_dir}/#{q}/out/#{id}.txt"
|
35
36
|
run_test(id, infile, outfile, expfile)
|
36
37
|
end
|
37
|
-
|
38
|
-
def show_result(is_success, input, result, expected)
|
39
|
-
print detail_str(input, result, expected) if @detail
|
40
|
-
puts result_str(is_success, result, expected)
|
41
|
-
end
|
42
38
|
end
|
43
39
|
end
|
44
40
|
end
|
@@ -15,18 +15,20 @@ module AtCoderFriends
|
|
15
15
|
|
16
16
|
def test_all
|
17
17
|
puts "***** test_all #{prg} (#{test_loc}) *****"
|
18
|
-
|
19
|
-
|
18
|
+
results = Dir["#{data_dir}/#{q}_*.in"].sort.map do |infile|
|
19
|
+
id = File.basename(infile, '.in').sub(/[^_]+_/, '')
|
20
|
+
test(id)
|
20
21
|
end
|
22
|
+
!results.empty? && results.all?
|
21
23
|
end
|
22
24
|
|
23
|
-
def test_one(
|
25
|
+
def test_one(id)
|
24
26
|
puts "***** test_one #{prg} (#{test_loc}) *****"
|
25
|
-
test(
|
27
|
+
test(id)
|
26
28
|
end
|
27
29
|
|
28
|
-
def test(
|
29
|
-
id = format('%<q>s_%<
|
30
|
+
def test(id)
|
31
|
+
id = format('%<q>s_%<id>s', q: q, id: id)
|
30
32
|
files = %w[in out exp].map { |ext| "#{data_dir}/#{id}.#{ext}" }
|
31
33
|
run_test(id, *files)
|
32
34
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'regression'
|
4
|
+
|
5
|
+
module AtCoderFriends
|
6
|
+
# tasks for regression
|
7
|
+
module Regression
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def check_diff
|
11
|
+
emit_dir = format(EMIT_DIR_FMT, now: Time.now.strftime('%Y%m%d%H%M%S'))
|
12
|
+
rmdir_force(emit_dir)
|
13
|
+
|
14
|
+
local_pbm_list.each do |contest, q, url|
|
15
|
+
pbm = scraping_agent(emit_dir, contest).fetch_problem(q, url)
|
16
|
+
pipeline(pbm)
|
17
|
+
end
|
18
|
+
|
19
|
+
system("diff -r #{EMIT_ORG_DIR} #{emit_dir}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
namespace :regression do
|
25
|
+
desc 'run regression check'
|
26
|
+
task :check_diff do
|
27
|
+
AtCoderFriends::Regression.check_diff
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'regression'
|
4
|
+
require 'at_coder_friends'
|
5
|
+
|
6
|
+
module AtCoderFriends
|
7
|
+
# tasks for regression
|
8
|
+
module Regression
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def check_parse(arg)
|
12
|
+
arg ||= 'fmt,smp,int'
|
13
|
+
list = local_pbm_list.map do |contest, q, url|
|
14
|
+
pbm = scraping_agent(REGRESSION_HOME, contest).fetch_problem(q, url)
|
15
|
+
Parser::Main.process(pbm)
|
16
|
+
tbl = {
|
17
|
+
'fmt' => !fmt?(pbm),
|
18
|
+
'smp' => pbm.samples.all? { |smp| smp.txt.empty? },
|
19
|
+
'int' => pbm.options.interactive,
|
20
|
+
'bin' => pbm.options.binary_values
|
21
|
+
}
|
22
|
+
[contest, q, tbl.values_at(*arg.split(','))]
|
23
|
+
end
|
24
|
+
report(list)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fmt?(pbm)
|
28
|
+
[Problem::SECTION_IN_FMT, Problem::SECTION_IO_FMT]
|
29
|
+
.any? { |key| pbm.sections[key]&.code_block&.size&.positive? }
|
30
|
+
end
|
31
|
+
|
32
|
+
def report(list)
|
33
|
+
list
|
34
|
+
.select { |_, _, flags| flags.any? }
|
35
|
+
.map { |c, q, flags| [c, q, flags.map { |f| f_to_s(f) }] }
|
36
|
+
.sort
|
37
|
+
.each { |args| puts args.flatten.join("\t") }
|
38
|
+
end
|
39
|
+
|
40
|
+
def f_to_s(f)
|
41
|
+
if f.is_a?(Array)
|
42
|
+
f
|
43
|
+
else
|
44
|
+
f ? '◯' : '-'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
namespace :regression do
|
51
|
+
desc 'checks page parse result'
|
52
|
+
task :check_parse, ['flags'] do |_, args|
|
53
|
+
flags = args[:flags]
|
54
|
+
AtCoderFriends::Regression.check_parse flags
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'json'
|
6
|
+
require 'csv'
|
7
|
+
require 'mechanize'
|
8
|
+
require 'at_coder_friends'
|
9
|
+
|
10
|
+
module AtCoderFriends
|
11
|
+
# tasks for regression
|
12
|
+
module Regression
|
13
|
+
module_function
|
14
|
+
|
15
|
+
CONTEST_LIST_URL = 'https://kenkoooo.com/atcoder/resources/contests.json'
|
16
|
+
REGRESSION_HOME =
|
17
|
+
File.expand_path(File.join(__dir__, '..', '..', 'regression'))
|
18
|
+
PAGES_DIR = File.join(REGRESSION_HOME, 'pages')
|
19
|
+
EMIT_ORG_DIR = File.join(REGRESSION_HOME, 'emit_org')
|
20
|
+
EMIT_DIR_FMT = File.join(REGRESSION_HOME, 'emit_%<now>s')
|
21
|
+
|
22
|
+
def contest_id_list
|
23
|
+
uri = URI.parse(CONTEST_LIST_URL)
|
24
|
+
json = Net::HTTP.get(uri)
|
25
|
+
contests = JSON.parse(json)
|
26
|
+
puts "Total #{contests.size} contests"
|
27
|
+
contests.map { |h| h['id'] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def local_pbm_list
|
31
|
+
Dir.glob(PAGES_DIR + '/**/*.html').map do |pbm_path|
|
32
|
+
contest = File.basename(File.dirname(pbm_path))
|
33
|
+
q = File.basename(pbm_path, '.html')
|
34
|
+
url = "file://#{pbm_path}"
|
35
|
+
[contest, q, url]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def pbm_list_from_file(file)
|
40
|
+
dat = File.join(REGRESSION_HOME, file)
|
41
|
+
CSV.read(dat, col_sep: "\t", headers: false).map do |contest, q|
|
42
|
+
pbm_path = File.join(PAGES_DIR, contest, "#{q}.html")
|
43
|
+
url = "file://#{pbm_path}"
|
44
|
+
[contest, q, url]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def scraping_agent(root, contest)
|
49
|
+
@ctx = Context.new({}, File.join(root, contest))
|
50
|
+
@ctx.scraping_agent
|
51
|
+
end
|
52
|
+
|
53
|
+
def agent
|
54
|
+
@agent ||= Mechanize.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def pipeline(pbm)
|
58
|
+
Parser::Main.process(pbm)
|
59
|
+
@ctx.generator.process(pbm)
|
60
|
+
@ctx.emitter.emit(pbm)
|
61
|
+
end
|
62
|
+
|
63
|
+
def rmdir_force(dir)
|
64
|
+
FileUtils.rm_r(dir) if Dir.exist?(dir)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|