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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.rubocop_todo.yml +1 -4
  4. data/Gemfile.lock +1 -1
  5. data/README.md +1 -1
  6. data/config/default.yml +3 -0
  7. data/docs/CONFIGURATION.md +74 -9
  8. data/lib/at_coder_friends.rb +10 -5
  9. data/lib/at_coder_friends/cli.rb +2 -5
  10. data/lib/at_coder_friends/context.rb +4 -0
  11. data/lib/at_coder_friends/emitter.rb +2 -2
  12. data/lib/at_coder_friends/generator/cxx_builtin.rb +191 -0
  13. data/lib/at_coder_friends/generator/main.rb +53 -0
  14. data/lib/at_coder_friends/generator/ruby_builtin.rb +128 -0
  15. data/lib/at_coder_friends/parser/binary.rb +39 -0
  16. data/lib/at_coder_friends/parser/constraints.rb +36 -0
  17. data/lib/at_coder_friends/parser/{format_parser.rb → input_format.rb} +42 -30
  18. data/lib/at_coder_friends/parser/interactive.rb +29 -0
  19. data/lib/at_coder_friends/parser/main.rb +6 -3
  20. data/lib/at_coder_friends/parser/sample_data.rb +24 -0
  21. data/lib/at_coder_friends/parser/section_wrapper.rb +49 -0
  22. data/lib/at_coder_friends/parser/{page_parser.rb → sections.rb} +44 -50
  23. data/lib/at_coder_friends/problem.rb +40 -24
  24. data/lib/at_coder_friends/scraping/agent.rb +1 -5
  25. data/lib/at_coder_friends/scraping/authentication.rb +2 -2
  26. data/lib/at_coder_friends/scraping/session.rb +1 -1
  27. data/lib/at_coder_friends/scraping/tasks.rb +2 -6
  28. data/lib/at_coder_friends/test_runner/base.rb +36 -31
  29. data/lib/at_coder_friends/test_runner/judge.rb +2 -6
  30. data/lib/at_coder_friends/test_runner/sample.rb +8 -6
  31. data/lib/at_coder_friends/version.rb +1 -1
  32. data/tasks/regression/check_diff.rake +29 -0
  33. data/tasks/regression/check_parse.rake +56 -0
  34. data/tasks/regression/regression.rb +67 -0
  35. data/tasks/regression/section_list.rake +41 -0
  36. data/tasks/regression/setup.rake +48 -0
  37. data/templates/cxx_builtin_default.cxx +26 -0
  38. data/templates/cxx_builtin_interactive.cxx +61 -0
  39. data/templates/ruby_builtin_default.rb +5 -0
  40. data/templates/ruby_builtin_interactive.rb +32 -0
  41. metadata +21 -8
  42. data/lib/at_coder_friends/cxx_generator.rb +0 -169
  43. data/lib/at_coder_friends/parser/constraints_parser.rb +0 -26
  44. data/lib/at_coder_friends/ruby_generator.rb +0 -97
  45. data/tasks/regression.rake +0 -163
@@ -1,45 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtCoderFriends
4
- SampleData = Struct.new(:no, :ext, :txt) do
5
- def initialize(no, ext, txt)
6
- super(no.to_i, ext, txt)
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
- InputDef = Struct.new(:container, :item, :names, :size) do
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
- Constraint = Struct.new(:name, :type, :value)
26
+ Options = Struct.new(:interactive, :binary_values)
17
27
 
18
- SourceCode = Struct.new(:ext, :txt)
28
+ SourceCode = Struct.new(:ext, :txt)
19
29
 
20
- # holds problem information
21
- class Problem
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 = nil
28
- @desc = ''
29
- @fmt = ''
30
- @smps = []
31
- @defs = []
35
+ @page = page
36
+ @sections = {}
37
+ @samples = []
38
+ @formats = []
32
39
  @constraints = []
33
- @srcs = []
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
- @smps << SampleData.new(no, ext, txt)
54
+ @samples << SampleData.new(no, ext, txt)
39
55
  end
40
56
 
41
57
  def add_src(ext, txt)
42
- @srcs << SourceCode.new(ext, txt)
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
@@ -19,7 +19,7 @@ module AtCoderFriends
19
19
  end
20
20
 
21
21
  def session_store
22
- @session_store ||= format(SESSION_STORE_FMT, user: config['user'])
22
+ @session_store ||= format(SESSION_STORE_FMT, user: ctx.config['user'])
23
23
  end
24
24
  end
25
25
  end
@@ -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
- Problem.new(q) do |pbm|
33
- page.search('br').each { |br| br.replace("\n") }
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
- end
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
- return nil unless cmd
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
- show_result(
49
- is_success,
50
- File.read(infile),
51
- File.read(outfile),
52
- File.read(expfile)
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 show_result(is_success, input, result, expected)
80
- print detail_str(input, result, expected)
81
- puts result_str(is_success, result, expected)
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.each do |infile|
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
- 1.upto(999) do |i|
19
- break unless test(i)
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(n)
25
+ def test_one(id)
24
26
  puts "***** test_one #{prg} (#{test_loc}) *****"
25
- test(n)
27
+ test(id)
26
28
  end
27
29
 
28
- def test(n)
29
- id = format('%<q>s_%<n>03d', q: q, n: n)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtCoderFriends
4
- VERSION = '0.5.2'
4
+ VERSION = '0.6.0'
5
5
  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