at_coder_friends 0.5.2 → 0.6.0
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/.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
|