daigaku 0.2.0 → 1.0.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 (88) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +7 -4
  3. data/CODE_OF_CONDUCT.md +77 -0
  4. data/README.md +11 -11
  5. data/bin/daigaku +6 -2
  6. data/daigaku.gemspec +22 -26
  7. data/lib/daigaku.rb +0 -1
  8. data/lib/daigaku/chapter.rb +3 -4
  9. data/lib/daigaku/coloring.rb +22 -27
  10. data/lib/daigaku/configuration.rb +25 -28
  11. data/lib/daigaku/congratulator.rb +17 -5
  12. data/lib/daigaku/course.rb +9 -8
  13. data/lib/daigaku/exceptions.rb +0 -2
  14. data/lib/daigaku/generator.rb +13 -15
  15. data/lib/daigaku/github_client.rb +5 -5
  16. data/lib/daigaku/loadable.rb +23 -14
  17. data/lib/daigaku/loading/chapters.rb +0 -2
  18. data/lib/daigaku/loading/courses.rb +0 -2
  19. data/lib/daigaku/loading/units.rb +0 -2
  20. data/lib/daigaku/markdown.rb +1 -0
  21. data/lib/daigaku/markdown/printer.rb +89 -0
  22. data/lib/daigaku/markdown/ruby_doc.rb +15 -15
  23. data/lib/daigaku/solution.rb +23 -15
  24. data/lib/daigaku/storeable.rb +11 -12
  25. data/lib/daigaku/task.rb +1 -1
  26. data/lib/daigaku/terminal.rb +3 -4
  27. data/lib/daigaku/terminal/cli.rb +8 -8
  28. data/lib/daigaku/terminal/courses.rb +22 -19
  29. data/lib/daigaku/terminal/output.rb +46 -53
  30. data/lib/daigaku/terminal/setup.rb +13 -18
  31. data/lib/daigaku/terminal/solutions.rb +27 -32
  32. data/lib/daigaku/terminal/welcome.rb +9 -11
  33. data/lib/daigaku/test.rb +7 -10
  34. data/lib/daigaku/test_result.rb +54 -21
  35. data/lib/daigaku/unit.rb +2 -4
  36. data/lib/daigaku/version.rb +1 -1
  37. data/lib/daigaku/views.rb +29 -33
  38. data/lib/daigaku/views/chapters_menu.rb +16 -20
  39. data/lib/daigaku/views/courses_menu.rb +12 -15
  40. data/lib/daigaku/views/main_menu.rb +23 -23
  41. data/lib/daigaku/views/menu.rb +14 -18
  42. data/lib/daigaku/views/splash.rb +11 -13
  43. data/lib/daigaku/views/subscriber.rb +38 -0
  44. data/lib/daigaku/views/task_view.rb +97 -80
  45. data/lib/daigaku/views/top_bar.rb +4 -10
  46. data/lib/daigaku/views/units_menu.rb +16 -21
  47. data/lib/daigaku/window.rb +12 -70
  48. data/spec/daigaku/chapter_spec.rb +23 -18
  49. data/spec/daigaku/coloring_spec.rb +0 -1
  50. data/spec/daigaku/configuration_spec.rb +54 -50
  51. data/spec/daigaku/congratulator_spec.rb +11 -8
  52. data/spec/daigaku/course_spec.rb +75 -52
  53. data/spec/daigaku/generator_spec.rb +24 -25
  54. data/spec/daigaku/github_client_spec.rb +17 -18
  55. data/spec/daigaku/loading/chapters_spec.rb +2 -3
  56. data/spec/daigaku/loading/courses_spec.rb +2 -3
  57. data/spec/daigaku/loading/units_spec.rb +4 -5
  58. data/spec/daigaku/markdown/ruby_doc_spec.rb +12 -6
  59. data/spec/daigaku/reference_solution_spec.rb +8 -10
  60. data/spec/daigaku/solution_spec.rb +21 -22
  61. data/spec/daigaku/storeable_spec.rb +12 -10
  62. data/spec/daigaku/task_spec.rb +3 -4
  63. data/spec/daigaku/terminal/cli_spec.rb +29 -21
  64. data/spec/daigaku/terminal/courses_spec.rb +104 -99
  65. data/spec/daigaku/terminal/output_spec.rb +44 -39
  66. data/spec/daigaku/terminal/setup_spec.rb +1 -3
  67. data/spec/daigaku/terminal/solutions_spec.rb +0 -2
  68. data/spec/daigaku/terminal/welcome_spec.rb +0 -2
  69. data/spec/daigaku/terminal_spec.rb +5 -7
  70. data/spec/daigaku/test_example_spec.rb +16 -14
  71. data/spec/daigaku/test_result_spec.rb +21 -25
  72. data/spec/daigaku/test_spec.rb +11 -12
  73. data/spec/daigaku/unit_spec.rb +24 -27
  74. data/spec/daigaku/views/chapters_menu_spec.rb +0 -1
  75. data/spec/daigaku/views/courses_menu_spec.rb +0 -1
  76. data/spec/daigaku/views/menu_spec.rb +1 -2
  77. data/spec/daigaku/views/task_view_spec.rb +0 -2
  78. data/spec/daigaku/views/units_menu_spec.rb +0 -1
  79. data/spec/daigaku/views_spec.rb +0 -1
  80. data/spec/daigaku_spec.rb +9 -12
  81. data/spec/path_helpers_spec.rb +11 -12
  82. data/spec/resource_helpers_spec.rb +11 -12
  83. data/spec/spec_helper.rb +3 -4
  84. data/spec/support/macros/content_helpers.rb +16 -17
  85. data/spec/support/macros/mock_helpers.rb +6 -6
  86. data/spec/support/macros/path_helpers.rb +15 -15
  87. data/spec/support/macros/resource_helpers.rb +34 -35
  88. metadata +32 -44
@@ -1,46 +1,41 @@
1
+ require 'os'
2
+ require_relative 'output'
3
+
1
4
  module Daigaku
2
5
  module Terminal
3
-
4
- require 'os'
5
- require_relative 'output'
6
-
7
6
  class Solutions < Thor
8
7
  include Terminal::Output
9
8
 
10
9
  desc 'solutions open [COURSE NAME]', 'Open the solutions folder of a course in a GUI window'
11
10
  def open(course_name = '')
12
- begin
13
- path = File.join(Daigaku.config.solutions_path, course_name)
14
-
15
- unless Dir.exist?(path)
16
- text = [
17
- "The course directory \"#{File.basename(path)}\" is not available in",
18
- "\"#{File.dirname(path)}\".\n",
19
- 'Hint:',
20
- 'Run "daigaku scaffold" to create empty solution files for all courses.'
21
- ]
22
- say_warning text.join("\n")
23
-
24
- unless Loading::Courses.load(Daigaku.config.courses_path).empty?
25
- Terminal::Courses.new.list
26
- end
27
-
28
- return
11
+ path = File.join(Daigaku.config.solutions_path, course_name)
12
+
13
+ unless Dir.exist?(path)
14
+ text = [
15
+ "The course directory \"#{File.basename(path)}\" is not available in",
16
+ "\"#{File.dirname(path)}\".\n",
17
+ 'Hint:',
18
+ 'Run "daigaku scaffold" to create empty solution files for all courses.'
19
+ ]
20
+ say_warning text.join("\n")
21
+
22
+ unless Loading::Courses.load(Daigaku.config.courses_path).empty?
23
+ Terminal::Courses.new.list
29
24
  end
30
25
 
31
- if OS.windows?
32
- system "explorer '#{path}'"
33
- elsif OS.mac?
34
- system "open '#{path}'"
35
- elsif OS.linux?
36
- system "xdg-open '#{path}'"
37
- end
38
- rescue ConfigurationError => e
39
- say_warning e.message
26
+ return
40
27
  end
41
- end
42
28
 
29
+ if OS.windows?
30
+ system "explorer '#{path}'"
31
+ elsif OS.mac?
32
+ system "open '#{path}'"
33
+ elsif OS.linux?
34
+ system "xdg-open '#{path}'"
35
+ end
36
+ rescue ConfigurationError => e
37
+ say_warning e.message
38
+ end
43
39
  end
44
-
45
40
  end
46
41
  end
@@ -1,22 +1,21 @@
1
1
  module Daigaku
2
2
  module Terminal
3
-
4
3
  class Welcome
5
4
  include Terminal::Output
6
5
 
7
6
  def self.run
8
- self.new.run
7
+ new.run
9
8
  end
10
9
 
11
10
  def self.about
12
- self.new.about
11
+ new.about
13
12
  end
14
13
 
15
14
  def run
16
15
  empty_line
17
16
  say Terminal.text :welcome
18
17
  empty_line
19
- say "For now, let's setup the daigaku paths."
18
+ say 'For now, lets setup the daigaku paths.'
20
19
  Daigaku::Terminal::Setup.new.init
21
20
 
22
21
  show_setup_list_announcement
@@ -37,7 +36,7 @@ module Daigaku
37
36
  empty_line
38
37
  say Terminal.text :about
39
38
  empty_line
40
- say %x{daigaku help}
39
+ say `daigaku help`
41
40
  end
42
41
 
43
42
  private
@@ -45,7 +44,7 @@ module Daigaku
45
44
  def show_setup_list_announcement
46
45
  command = 'daigaku setup list'
47
46
  text = [
48
- "The courses path and solutions path have been added to your settings.",
47
+ 'The courses path and solutions path have been added to your settings.',
49
48
  "Just list your current settings with the \"#{command}\" command:"
50
49
  ].join("\n")
51
50
 
@@ -56,7 +55,7 @@ module Daigaku
56
55
  command = 'daigaku courses list'
57
56
  text = [
58
57
  "Well done. Now, type \"#{command}\" to see what courses are",
59
- "available in your daigaku folder:"
58
+ 'available in your daigaku folder:'
60
59
  ].join("\n")
61
60
 
62
61
  get_command(command, text)
@@ -65,7 +64,7 @@ module Daigaku
65
64
  def show_courses_download_announcement
66
65
  command = 'daigaku courses download'
67
66
  text = [
68
- "Oh! You don't have any courses, yet?",
67
+ 'Oh! You dont have any courses, yet?',
69
68
  "Just enter \"#{command}\" to download the basic Daigaku course:"
70
69
  ].join("\n")
71
70
 
@@ -75,7 +74,7 @@ module Daigaku
75
74
  def show_solutions_open_announcement
76
75
  command = 'daigaku solutions open'
77
76
  text = [
78
- "When downloading a course, Daigaku scaffolds empty solution files",
77
+ 'When downloading a course, Daigaku scaffolds empty solution files',
79
78
  "for your code on the fly.\n",
80
79
  "Type \"#{command}\" to open your solutions folder:"
81
80
  ].join("\n")
@@ -86,13 +85,12 @@ module Daigaku
86
85
  def show_learn_announcement
87
86
  command = 'daigaku learn'
88
87
  text = [
89
- "Congratulations! You learned the first steps of using daigaku.",
88
+ 'Congratulations! You learned the first steps of using daigaku.',
90
89
  "To continue and start learning Ruby type \"#{command}\":"
91
90
  ].join("\n")
92
91
 
93
92
  get_command(command, text)
94
93
  end
95
94
  end
96
-
97
95
  end
98
96
  end
@@ -1,25 +1,24 @@
1
- module Daigaku
2
- require 'fileutils'
1
+ require 'fileutils'
3
2
 
3
+ module Daigaku
4
4
  class Test
5
+ CODE_REGEX = /\[\['solution::code'\]\]/
5
6
 
6
7
  attr_reader :path
7
8
 
8
- CODE_REGEX = /\[\['solution::code'\]\]/
9
-
10
9
  def initialize(path)
11
10
  @unit_path = path
12
- @path = Dir[File.join(path, '*spec.rb')].first
11
+ @path = Dir[File.join(path, '*spec.rb')].first
13
12
  end
14
13
 
15
14
  def run(solution_code)
16
- spec_code = File.read(@path)
15
+ spec_code = File.read(@path)
17
16
  patched_spec_code = insert_code(spec_code, solution_code.to_s)
18
17
 
19
18
  temp_spec = File.join(File.dirname(@path), "temp_#{File.basename(@path)}")
20
19
  create_temp_spec(temp_spec, patched_spec_code)
21
20
 
22
- result = %x{ rspec --color --format j #{temp_spec} }
21
+ result = `rspec --no-color --order defined --format j #{temp_spec}`
23
22
  remove_file(temp_spec)
24
23
 
25
24
  TestResult.new(result)
@@ -33,14 +32,12 @@ module Daigaku
33
32
 
34
33
  def create_temp_spec(path, content)
35
34
  base_path = File.dirname(path)
36
- FileUtils.mkdir_p(base_path) unless Dir.exist?(base_path)
35
+ FileUtils.makedirs(base_path) unless Dir.exist?(base_path)
37
36
  File.open(path, 'w') { |f| f.puts content }
38
37
  end
39
38
 
40
39
  def remove_file(path)
41
40
  FileUtils.rm(path) if File.exist?(path)
42
41
  end
43
-
44
42
  end
45
-
46
43
  end
@@ -1,25 +1,33 @@
1
- module Daigaku
1
+ require 'json'
2
2
 
3
+ module Daigaku
3
4
  class TestResult
4
- require 'json'
5
+ CODE_ERROR_MESSAGE = ':( You got an error in your code!'.freeze
5
6
 
6
7
  attr_reader :examples, :example_count, :failure_count
7
8
 
8
9
  def initialize(result_json)
9
10
  @result = begin
10
11
  JSON.parse(result_json, symbolize_names: true)
11
- rescue
12
- syntax_error_json
12
+ rescue => error
13
+ syntax_error_json(error)
13
14
  end
14
15
 
15
- @example_count = @result[:summary][:example_count]
16
- @failure_count = @result[:summary][:failure_count]
16
+ @example_count = @result.dig(:summary, :example_count)
17
+ @failure_count = @result.dig(:summary, :failure_count)
18
+ error_count = @result.dig(:summary, :errors_outside_of_examples_count) || 0
19
+
20
+ if error_count > 0
21
+ @failure_count = error_count
22
+ details = error_details(@result)
23
+ @result = error_json(details)
24
+ end
17
25
 
18
26
  @examples = @result[:examples].map do |example|
19
27
  description = example[:full_description]
20
- status = example[:status]
21
- exception = example[:exception]
22
- message = exception ? exception[:message] : nil
28
+ status = example[:status]
29
+ exception = example[:exception]
30
+ message = exception ? exception[:message] : nil
23
31
 
24
32
  TestExample.new(description: description, status: status, message: message)
25
33
  end
@@ -47,40 +55,65 @@ module Daigaku
47
55
 
48
56
  def build_failed_summary
49
57
  message = examples.map do |example|
50
- "#{example.description}\n#{example.status}: #{example.message}"
58
+ "#{example.description}\n#{example.status}: #{example.message}".strip
51
59
  end
52
60
 
53
- summary = message.map(&:strip).join("\n" * 3)
61
+ message.join("\n" * 3)
54
62
  end
55
63
 
56
- def syntax_error_json
64
+ def syntax_error_json(error)
65
+ details = failure_details(error)
66
+ error_json(details)
67
+ end
68
+
69
+ def error_details(result)
70
+ result[:messages]
71
+ .first
72
+ .split('\n')
73
+ .each_with_index
74
+ .select { |line, index| index > 0 && line.matches?(/temp_.+\.rb/) }
75
+ .first.to_s
76
+ end
77
+
78
+ def error_json(details)
57
79
  {
58
80
  summary: {},
59
81
  examples: [
60
82
  {
61
- status: 'failed',
62
- exception: { message: ":( You got a syntax error in your code!" }
83
+ status: TestExample::FAILED,
84
+ exception: { message: "#{CODE_ERROR_MESSAGE}\n\n#{details}" }
63
85
  }
64
86
  ]
65
87
  }
66
88
  end
89
+
90
+ def failure_details(error)
91
+ line = error.backtrace.first
92
+ error_message = remove_colorization(error.message)
93
+ "#{error.class} in #{line}:\n#{error_message}"
94
+ end
95
+
96
+ def remove_colorization(text)
97
+ text.gsub(/\x1b\[[0-9]*m/i, '')
98
+ end
67
99
  end
68
100
 
69
101
  class TestExample
102
+ PASSED = 'passed'.freeze
103
+ FAILED = 'failed'.freeze
70
104
 
71
105
  attr_reader :description, :status, :message
72
106
 
73
- EXAMPLE_PASSED_MESSAGE = "Your code passed this requirement."
107
+ EXAMPLE_PASSED_MESSAGE = 'Your code passed this requirement.'.freeze
74
108
 
75
- def initialize(args = {})
76
- @description = args[:description]
77
- @status = args[:status]
78
- @message = args[:message] || EXAMPLE_PASSED_MESSAGE
109
+ def initialize(status:, description: nil, message: nil)
110
+ @description = description
111
+ @status = status
112
+ @message = message || EXAMPLE_PASSED_MESSAGE
79
113
  end
80
114
 
81
115
  def passed?
82
- @status == 'passed'
116
+ @status == PASSED
83
117
  end
84
118
  end
85
-
86
119
  end
@@ -1,10 +1,9 @@
1
1
  module Daigaku
2
2
  class Unit
3
-
4
3
  attr_reader :title, :task
5
4
 
6
5
  def initialize(path)
7
- @path = path
6
+ @path = path
8
7
  @title = File.basename(path).gsub(/\_+/, ' ')
9
8
  end
10
9
 
@@ -17,12 +16,11 @@ module Daigaku
17
16
  end
18
17
 
19
18
  def solution
20
- @solution = Solution.new(@path)
19
+ @solution ||= Solution.new(@path)
21
20
  end
22
21
 
23
22
  def mastered?
24
23
  solution.verified?
25
24
  end
26
-
27
25
  end
28
26
  end
@@ -1,3 +1,3 @@
1
1
  module Daigaku
2
- VERSION = "0.2.0"
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -1,51 +1,47 @@
1
1
  require 'curses'
2
- require 'active_support/concern'
3
2
  require 'daigaku/views/top_bar'
4
3
 
5
4
  module Daigaku
6
5
  module Views
7
- extend ActiveSupport::Concern
6
+ include Curses
8
7
 
9
- included do
10
- include Curses
11
-
12
- def reset_menu_position
13
- @position = 0
14
- end
15
-
16
- private
8
+ def reset_menu_position
9
+ @position = 0
10
+ end
17
11
 
18
- def default_window(height = nil, width = nil, top = 0, left = 0)
19
- init_screen
12
+ private
20
13
 
21
- noecho
22
- crmode
23
- curs_set(0) # invisible cursor
14
+ def default_window(height = nil, width = nil, top = 0, left = 0)
15
+ init_screen
24
16
 
25
- height ||= lines
26
- width ||= cols + 1
17
+ noecho
18
+ crmode
19
+ curs_set(0) # invisible cursor
27
20
 
28
- window = Daigaku::Window.new(height, width, top, left)
21
+ height ||= lines
22
+ width ||= cols + 1
29
23
 
30
- Curses.lines.times do |line|
31
- window.setpos(line, 0)
32
- window.clear_line
33
- end
24
+ window = Daigaku::Window.new(height, width, top, left)
34
25
 
35
- window.keypad(true)
36
- window.scrollok(true)
37
- window.refresh
38
- window
26
+ Curses.lines.times do |line|
27
+ window.setpos(line, 0)
28
+ window.clear_line
39
29
  end
40
30
 
41
- def sub_window_below_top_bar(window, top_bar)
42
- top_bar.show
43
- top = top_bar.height
44
- sub_window = window.subwin(window.maxy - top, window.maxx, top, 0)
45
- sub_window.keypad(true)
46
- sub_window
47
- end
31
+ window.keypad(true)
32
+ window.scrollok(true)
33
+ window.refresh
34
+ window
48
35
  end
49
36
 
37
+ def sub_window_below_top_bar(window, top_bar)
38
+ top_bar.show
39
+
40
+ top = top_bar.height
41
+ sub_window = window.subwin(window.maxy - top, window.maxx, top, 0)
42
+
43
+ sub_window.keypad(true)
44
+ sub_window
45
+ end
50
46
  end
51
47
  end
@@ -2,9 +2,7 @@ require 'daigaku/views/menu'
2
2
 
3
3
  module Daigaku
4
4
  module Views
5
-
6
5
  class ChaptersMenu < Menu
7
-
8
6
  private
9
7
 
10
8
  def before_enter(*args)
@@ -12,8 +10,8 @@ module Daigaku
12
10
  end
13
11
 
14
12
  def before_reenter(*args)
15
- @course = args[0]
16
- @chapter = args[1]
13
+ @course = args[0]
14
+ @chapter = args[1]
17
15
  @position = @course.chapters.find_index(@chapter)
18
16
  end
19
17
 
@@ -24,20 +22,20 @@ module Daigaku
24
22
  def interact_with(window)
25
23
  while char = window.getch
26
24
  case char
27
- when KEY_UP
28
- @position -= 1
29
- broadcast(:reset_menu_position)
30
- when KEY_DOWN
31
- @position += 1
32
- broadcast(:reset_menu_position)
33
- when 10 # Enter
34
- broadcast(:enter, @course, models[@position])
35
- return
36
- when 263 # Backspace
37
- broadcast(:reenter, @course)
38
- return
39
- when 27 # ESC
40
- exit
25
+ when KEY_UP
26
+ @position -= 1
27
+ broadcast(:reset_menu_position)
28
+ when KEY_DOWN
29
+ @position += 1
30
+ broadcast(:reset_menu_position)
31
+ when 10 # Enter
32
+ broadcast(:enter, @course, models[@position])
33
+ return
34
+ when 263 # Backspace
35
+ broadcast(:reenter, @course)
36
+ return
37
+ when 27 # ESC
38
+ exit
41
39
  end
42
40
 
43
41
  @position = items.length - 1 if @position < 0
@@ -53,8 +51,6 @@ module Daigaku
53
51
  def items
54
52
  models.map(&:title)
55
53
  end
56
-
57
54
  end
58
-
59
55
  end
60
56
  end