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