daigaku 0.0.1

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +5 -0
  5. data/README.md +62 -0
  6. data/Rakefile +2 -0
  7. data/bin/daigaku +12 -0
  8. data/daigaku.gemspec +37 -0
  9. data/lib/daigaku.rb +27 -0
  10. data/lib/daigaku/chapter.rb +23 -0
  11. data/lib/daigaku/configuration.rb +86 -0
  12. data/lib/daigaku/course.rb +23 -0
  13. data/lib/daigaku/database.rb +64 -0
  14. data/lib/daigaku/exceptions.rb +19 -0
  15. data/lib/daigaku/generator.rb +53 -0
  16. data/lib/daigaku/loadable.rb +23 -0
  17. data/lib/daigaku/loading/chapters.rb +9 -0
  18. data/lib/daigaku/loading/courses.rb +9 -0
  19. data/lib/daigaku/loading/units.rb +9 -0
  20. data/lib/daigaku/reference_solution.rb +10 -0
  21. data/lib/daigaku/solution.rb +42 -0
  22. data/lib/daigaku/task.rb +10 -0
  23. data/lib/daigaku/terminal.rb +11 -0
  24. data/lib/daigaku/terminal/cli.rb +59 -0
  25. data/lib/daigaku/terminal/courses.rb +114 -0
  26. data/lib/daigaku/terminal/output.rb +72 -0
  27. data/lib/daigaku/terminal/setup.rb +115 -0
  28. data/lib/daigaku/terminal/solutions.rb +46 -0
  29. data/lib/daigaku/terminal/texts/about.txt +19 -0
  30. data/lib/daigaku/terminal/texts/courses_empty.txt +3 -0
  31. data/lib/daigaku/terminal/texts/hint_course_download.txt +13 -0
  32. data/lib/daigaku/terminal/texts/welcome.txt +12 -0
  33. data/lib/daigaku/terminal/welcome.rb +98 -0
  34. data/lib/daigaku/test.rb +46 -0
  35. data/lib/daigaku/test_result.rb +69 -0
  36. data/lib/daigaku/unit.rb +28 -0
  37. data/lib/daigaku/version.rb +3 -0
  38. data/lib/daigaku/views.rb +59 -0
  39. data/lib/daigaku/views/chapters_menu.rb +91 -0
  40. data/lib/daigaku/views/courses_menu.rb +87 -0
  41. data/lib/daigaku/views/main_menu.rb +37 -0
  42. data/lib/daigaku/views/splash.rb +57 -0
  43. data/lib/daigaku/views/task_view.rb +206 -0
  44. data/lib/daigaku/views/top_bar.rb +48 -0
  45. data/lib/daigaku/views/units_menu.rb +92 -0
  46. data/lib/daigaku/window.rb +160 -0
  47. data/spec/daigaku/chapter_spec.rb +76 -0
  48. data/spec/daigaku/configuration_spec.rb +161 -0
  49. data/spec/daigaku/course_spec.rb +75 -0
  50. data/spec/daigaku/database_spec.rb +79 -0
  51. data/spec/daigaku/generator_spec.rb +82 -0
  52. data/spec/daigaku/loading/chapters_spec.rb +16 -0
  53. data/spec/daigaku/loading/courses_spec.rb +16 -0
  54. data/spec/daigaku/loading/units_spec.rb +21 -0
  55. data/spec/daigaku/reference_solution_spec.rb +23 -0
  56. data/spec/daigaku/solution_spec.rb +79 -0
  57. data/spec/daigaku/task_spec.rb +23 -0
  58. data/spec/daigaku/terminal/cli_spec.rb +51 -0
  59. data/spec/daigaku/terminal/courses_spec.rb +60 -0
  60. data/spec/daigaku/terminal/output_spec.rb +123 -0
  61. data/spec/daigaku/terminal/setup_spec.rb +10 -0
  62. data/spec/daigaku/terminal/solutions_spec.rb +8 -0
  63. data/spec/daigaku/terminal/welcome_spec.rb +12 -0
  64. data/spec/daigaku/terminal_spec.rb +14 -0
  65. data/spec/daigaku/test_example_spec.rb +54 -0
  66. data/spec/daigaku/test_result_spec.rb +81 -0
  67. data/spec/daigaku/test_spec.rb +48 -0
  68. data/spec/daigaku/unit_spec.rb +85 -0
  69. data/spec/daigaku/views/chapters_menu_spec.rb +8 -0
  70. data/spec/daigaku/views/courses_menu_spec.rb +8 -0
  71. data/spec/daigaku/views/task_view_spec.rb +7 -0
  72. data/spec/daigaku/views/units_menu_spec.rb +8 -0
  73. data/spec/daigaku/views_spec.rb +23 -0
  74. data/spec/daigaku_spec.rb +57 -0
  75. data/spec/path_helpers_spec.rb +60 -0
  76. data/spec/resource_helpers_spec.rb +33 -0
  77. data/spec/spec_helper.rb +28 -0
  78. data/spec/support/macros/content_helpers.rb +129 -0
  79. data/spec/support/macros/mock_helpers.rb +20 -0
  80. data/spec/support/macros/path_helpers.rb +133 -0
  81. data/spec/support/macros/resource_helpers.rb +119 -0
  82. data/spec/support/macros/test_helpers.rb +6 -0
  83. metadata +361 -0
@@ -0,0 +1,160 @@
1
+ require 'curses'
2
+
3
+ module Daigaku
4
+ class Window < Curses::Window
5
+
6
+ COLOR_TEXT = Curses::COLOR_YELLOW unless defined? COLOR_TEXT
7
+ COLOR_TEXT_EMPHASIZE = Curses::COLOR_CYAN unless defined? COLOR_TEXT_EMPHASIZE
8
+ COLOR_HEADING = Curses::COLOR_WHITE unless defined? COLOR_HEADING
9
+ COLOR_RED = Curses::COLOR_BLUE unless defined? COLOR_RED
10
+ COLOR_GREEN = Curses::COLOR_MAGENTA unless defined? COLOR_GREEN
11
+ COLOR_YELLOW = Curses::COLOR_RED unless defined? COLOR_YELLOW
12
+
13
+ BACKGROUND = Curses::COLOR_WHITE unless defined? BACKGROUND
14
+ FONT = Curses::COLOR_BLACK unless defined? FONT
15
+ FONT_HEADING = Curses::COLOR_MAGENTA unless defined? FONT_HEADING
16
+ FONT_EMPHASIZE = Curses::COLOR_BLUE unless defined? FONT_EMPHASIZE
17
+ RED = Curses::COLOR_RED unless defined? RED
18
+ GREEN = Curses::COLOR_GREEN unless defined? GREEN
19
+ YELLOW = Curses::COLOR_YELLOW unless defined? YELLOW
20
+
21
+ def initialize(height = Curses.lines, width = Curses.cols, top = 0, left = 0)
22
+ super(height, width, top, left)
23
+ init_colors
24
+ end
25
+
26
+ def write(text, color = COLOR_TEXT, text_decoration = Curses::A_NORMAL )
27
+ self.attron(Curses.color_pair(color) | text_decoration) { self << text.to_s }
28
+ end
29
+
30
+ def emphasize(text, text_decoration = Curses::A_NORMAL)
31
+ write(text, Window::COLOR_TEXT_EMPHASIZE, text_decoration)
32
+ end
33
+
34
+ def heading(text, text_decoration = Curses::A_UNDERLINE)
35
+ write(text, Window::COLOR_HEADING, text_decoration)
36
+ end
37
+
38
+ def red(text, text_decoration = Curses::A_NORMAL, options = {})
39
+ colored(text, Window::COLOR_RED, text_decoration, options)
40
+ end
41
+
42
+ def yellow(text, text_decoration = Curses::A_NORMAL, options = {})
43
+ colored(text, Window::COLOR_YELLOW, text_decoration, options)
44
+ end
45
+
46
+ def green(text, text_decoration = Curses::A_NORMAL, options = {})
47
+ colored(text, Window::COLOR_GREEN, text_decoration, options)
48
+ end
49
+
50
+ def colored(text, color, text_decoration = Curses::A_NORMAL, options = {})
51
+ if options[:full_line]
52
+ clear_line(
53
+ color: color,
54
+ text_decoration: Curses::A_STANDOUT,
55
+ start_pos: 1,
56
+ end_pos: maxx - 2
57
+ )
58
+
59
+ prefix = ' '
60
+ end
61
+
62
+ write("#{prefix}#{text}", color, text_decoration)
63
+ end
64
+
65
+ # clear_line(options = {})
66
+ # options: [:color, :text_decoration, :start_pos, :end_pos]
67
+ def clear_line(options = {})
68
+ color = options[:color] || COLOR_TEXT
69
+ text_decoration = options[:text_decoration] || Curses::A_NORMAL
70
+ start = options[:start_pos] || 0
71
+ stop = options[:end_pos] || maxx
72
+
73
+ x = curx
74
+ setpos(cury, start)
75
+ write(' ' * (stop - 1), color, text_decoration)
76
+ setpos(cury, x)
77
+ refresh
78
+ end
79
+
80
+ def print_indicator(object, text = ' ', text_decoration = Curses::A_STANDOUT)
81
+ if object.respond_to?(:mastered?) && object.respond_to?(:started?)
82
+ if object.mastered?
83
+ green(text, text_decoration)
84
+ write ' '
85
+ elsif object.started?
86
+ yellow(text, text_decoration)
87
+ write ' '
88
+ else
89
+ red(text, text_decoration)
90
+ write ' '
91
+ end
92
+ elsif object.respond_to?(:mastered?)
93
+ if object.mastered?
94
+ green(text, text_decoration)
95
+ write ' '
96
+ else
97
+ red(text, text_decoration)
98
+ write ' '
99
+ end
100
+ end
101
+ end
102
+
103
+ def print_markdown(text)
104
+ clear_line
105
+
106
+ h1 = /^\#{1}[^#]+/ # '# heading'
107
+ h2 = /^\#{2}[^#]+/ # '## sub heading'
108
+ bold = /(\*[^*]*\*)/ # '*text*'
109
+ line = /^-{3,}/ # '---' vertical line
110
+ code = /(\`*\`)/ # '`code line`'
111
+
112
+ case text
113
+ when h1
114
+ heading(text.gsub(/^#\s?/, ''))
115
+ when h2
116
+ text_decoration = Curses::A_UNDERLINE | Curses::A_NORMAL
117
+ emphasize(text.gsub(/^##\s?/, ''), text_decoration)
118
+ when bold || code
119
+ emphasized = false
120
+ highlighted = false
121
+
122
+ text.chars.each do |char|
123
+ if char == '*'
124
+ emphasized = !emphasized
125
+ next
126
+ end
127
+
128
+ if char == '`'
129
+ highlighted = !highlighted
130
+ next
131
+ end
132
+
133
+ if emphasized
134
+ emphasize(char)
135
+ elsif highlighted
136
+ red(char)
137
+ else
138
+ write(char)
139
+ end
140
+ end
141
+ when line
142
+ write('-' * (Curses.cols - 2))
143
+ else
144
+ write(text)
145
+ end
146
+ end
147
+
148
+ protected
149
+
150
+ def init_colors
151
+ Curses.start_color
152
+ Curses.init_pair(COLOR_TEXT, FONT, BACKGROUND)
153
+ Curses.init_pair(COLOR_TEXT_EMPHASIZE, FONT_EMPHASIZE, BACKGROUND)
154
+ Curses.init_pair(COLOR_HEADING, FONT_HEADING, BACKGROUND)
155
+ Curses.init_pair(COLOR_RED, RED, BACKGROUND)
156
+ Curses.init_pair(COLOR_GREEN, GREEN, BACKGROUND)
157
+ Curses.init_pair(COLOR_YELLOW, YELLOW, BACKGROUND)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Daigaku::Chapter do
4
+
5
+ it { is_expected.to respond_to :title }
6
+ it { is_expected.to respond_to :units }
7
+ it { is_expected.to respond_to :path }
8
+
9
+ it { is_expected.to respond_to :mastered? }
10
+ it { is_expected.to respond_to :started? }
11
+
12
+ let(:chapter_path) { chapter_dirs(course_dir_names.first).first }
13
+
14
+ before(:all) do
15
+ prepare_solutions
16
+ Daigaku.config.solutions_path = solutions_basepath
17
+ end
18
+
19
+ subject { Daigaku::Chapter.new(chapter_path) }
20
+
21
+ it "has the prescribed title" do
22
+ expect(subject.title).to eq chapter_titles.first
23
+ end
24
+
25
+ it "has the prescribed path" do
26
+ expect(subject.path).to eq chapter_path
27
+ end
28
+
29
+ it "is not started by default" do
30
+ expect(subject.started?).to be_falsey
31
+ end
32
+
33
+ it "is not mastered by default" do
34
+ expect(subject.mastered?).to be_falsey
35
+ end
36
+
37
+ describe "#units" do
38
+ it "loads the prescribed number of units" do
39
+ course_name = course_dir_names.first
40
+ chapter_name = File.basename(chapter_path)
41
+ units_count = available_units(course_name, chapter_name).count
42
+ expect(subject.units.count).to eq units_count
43
+ end
44
+
45
+ it "lazy-loads the units" do
46
+ expect(subject.instance_variable_get(:@units)).to be_nil
47
+ subject.units
48
+ expect(subject.instance_variable_get(:@units)).not_to be_nil
49
+ end
50
+ end
51
+
52
+ describe "#started?" do
53
+ it "returns true if at least one unit has been verified" do
54
+ allow(subject.units.first).to receive(:mastered?) { true }
55
+ expect(subject.started?).to be true
56
+ end
57
+
58
+ it "returns false if no unit has been verified" do
59
+ allow_any_instance_of(Daigaku::Unit).to receive(:mastered?) { false }
60
+ expect(subject.started?).to be false
61
+ end
62
+ end
63
+
64
+ describe "#mastered?" do
65
+ it "returns true if all units have been verified" do
66
+ subject.units.each { |unit| unit.solution.verify! }
67
+ expect(subject.mastered?).to be true
68
+ end
69
+
70
+ it "returns false unless all units have been verified" do
71
+ allow_any_instance_of(Daigaku::Unit).to receive(:mastered?) { false }
72
+ allow(subject.units.first).to receive(:mastered?) { true }
73
+ expect(subject.mastered?).to be false
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+
3
+ describe Daigaku::Configuration do
4
+
5
+ subject { Daigaku::Configuration.send(:new) }
6
+
7
+ it { is_expected.to respond_to :solutions_path }
8
+ it { is_expected.to respond_to :solutions_path= }
9
+ it { is_expected.to respond_to :courses_path }
10
+ it { is_expected.to respond_to :courses_path= }
11
+ it { is_expected.to respond_to :storage_file }
12
+ it { is_expected.to respond_to :save }
13
+ it { is_expected.to respond_to :import! }
14
+ it { is_expected.to respond_to :summary }
15
+
16
+ before do
17
+ subject.instance_variable_set(:@storage_file, local_storage_file)
18
+ end
19
+
20
+ describe "#courses_path" do
21
+ it "returns the appropriate initial local courses path" do
22
+ courses_path = File.expand_path('~/.daigaku/courses', __FILE__)
23
+ expect(subject.courses_path).to eq courses_path
24
+ end
25
+
26
+ it "returns the appropriate set courses path" do
27
+ subject.courses_path = local_courses_path
28
+ expect(subject.courses_path).to eq local_courses_path
29
+ end
30
+ end
31
+
32
+ describe "#solutions_path=" do
33
+ it "raises an error if the given path is no existent dir" do
34
+ expect { subject.solutions_path = "no/existent/dir" }
35
+ .to raise_error(Daigaku::ConfigurationError)
36
+ end
37
+ end
38
+
39
+ describe "#solutions_path" do
40
+ it "raises an error if not set" do
41
+ expect { subject.solutions_path }
42
+ .to raise_error(Daigaku::ConfigurationError)
43
+ end
44
+
45
+ it "returns the solutions path if set" do
46
+ subject.solutions_path = test_basepath
47
+ expect { subject.solutions_path }.not_to raise_error
48
+ end
49
+ end
50
+
51
+ describe "#storage_path" do
52
+ it "returns the appropriate path to the storge file" do
53
+ expect(subject.storage_file).to eq local_storage_file
54
+ end
55
+ end
56
+
57
+ describe "#save" do
58
+ it "saves the configured courses path to the daigaku database" do
59
+ subject.courses_path = local_courses_path
60
+ subject.save
61
+
62
+ expect(Daigaku::Database.courses_path).to eq local_courses_path
63
+ end
64
+
65
+ it "saves the configured solution_path to the daigaku database" do
66
+ path = File.join(test_basepath, 'test_solutions')
67
+ FileUtils.makedirs(path)
68
+ subject.solutions_path = path
69
+ subject.save
70
+
71
+ expect(Daigaku::Database.solutions_path).to eq path
72
+ end
73
+
74
+ it "does not save the storage file path" do
75
+ subject.save
76
+ expect(Daigaku::Database.storage_file).to be_nil
77
+ end
78
+ end
79
+
80
+ describe "#import!" do
81
+ context "with non-existent daigaku database entries:" do
82
+ before do
83
+ FileUtils.rm(local_storage_file) if File.exist?(local_storage_file)
84
+ end
85
+
86
+ it "uses the default configuration" do
87
+ Daigaku::Database.courses_path = nil
88
+ Daigaku::Database.solutions_path = nil
89
+ subject.instance_variable_set(:@courses_path, local_courses_path)
90
+
91
+ loaded_config = subject.import!
92
+
93
+ expect(loaded_config.courses_path).to eq local_courses_path
94
+ expect { loaded_config.solutions_path }
95
+ .to raise_error Daigaku::ConfigurationError
96
+ end
97
+ end
98
+
99
+ context "with existing daigaku database file:" do
100
+ it "returns a Daigaku::Configuration" do
101
+ expect(subject.import!).to be_a Daigaku::Configuration
102
+ end
103
+
104
+ it "loads the daigaku database entries into the configuration" do
105
+ wanted_courses_path = "/courses/path"
106
+ wanted_solutions_path = solutions_basepath
107
+ temp_solutions_path = File.join(solutions_basepath, 'temp')
108
+ FileUtils.makedirs(wanted_solutions_path)
109
+ FileUtils.makedirs(temp_solutions_path)
110
+
111
+ # save wanted settings
112
+ subject.courses_path = wanted_courses_path
113
+ subject.solutions_path = wanted_solutions_path
114
+ subject.save
115
+
116
+ # overwrite in memory settings
117
+ subject.courses_path = '/some/other/path/'
118
+ subject.solutions_path = temp_solutions_path
119
+
120
+ # fetch stored settings
121
+ subject.import!
122
+ expect(File.exist?(local_storage_file)).to be_truthy
123
+ expect(subject.courses_path).to eq wanted_courses_path
124
+ expect(subject.solutions_path).to eq wanted_solutions_path
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#summary' do
130
+ before do
131
+ subject.courses_path = "wanted/courses/path"
132
+ subject.solutions_path = solutions_basepath
133
+ @summary = subject.summary
134
+ end
135
+
136
+ it "returns a string" do
137
+ expect(@summary).to be_a String
138
+ end
139
+
140
+ it "returns a string with all properties but @configuration_file" do
141
+ expect(@summary.lines.count).to eq subject.instance_variables.count - 1
142
+ end
143
+
144
+ it "returns a string including the courses path" do
145
+ expect(@summary =~ /courses path/).to be_truthy
146
+ end
147
+
148
+ it "returns a string including the courses path" do
149
+ expect(@summary =~ /solutions path/).to be_truthy
150
+ end
151
+ end
152
+
153
+ describe '#initial_course' do
154
+ it { is_expected.to respond_to :initial_course }
155
+
156
+ it "returns the initial course github repo partial path" do
157
+ expect(subject.initial_course).to eq 'daigaku-ruby/Get_started_with_Ruby'
158
+ end
159
+ end
160
+
161
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Daigaku::Course do
4
+
5
+ it { is_expected.to respond_to :title }
6
+ it { is_expected.to respond_to :chapters }
7
+ it { is_expected.to respond_to :path }
8
+ it { is_expected.to respond_to :author }
9
+ it { is_expected.to respond_to :link }
10
+
11
+ it { is_expected.to respond_to :mastered? }
12
+ it { is_expected.to respond_to :started? }
13
+
14
+ let(:course_path) { course_dirs.first }
15
+
16
+ before(:all) do
17
+ prepare_solutions
18
+ Daigaku.config.solutions_path = solutions_basepath
19
+ end
20
+
21
+ subject { Daigaku::Course.new(course_path) }
22
+
23
+ it "has the prescribed title" do
24
+ expect(subject.title).to eq course_titles.first
25
+ end
26
+
27
+ it "has the prescribed path" do
28
+ expect(subject.path).to eq course_path
29
+ end
30
+
31
+ it "is not started by default" do
32
+ expect(subject.started?).to be_falsey
33
+ end
34
+
35
+ it "is not mastered by default" do
36
+ expect(subject.mastered?).to be_falsey
37
+ end
38
+
39
+ describe "#chapters" do
40
+ it "loads the prescribed number of chapters" do
41
+ expect(subject.chapters.count).to eq available_chapters(course_path).count
42
+ end
43
+
44
+ it "lazy-loads the chapters" do
45
+ expect(subject.instance_variable_get(:@chapters)).to be_nil
46
+ subject.chapters
47
+ expect(subject.instance_variable_get(:@chapters)).not_to be_nil
48
+ end
49
+ end
50
+
51
+ describe "#started?" do
52
+ it "returns true if at least one chapter has been started" do
53
+ allow(subject.chapters.first).to receive(:started?) { true }
54
+ expect(subject.started?).to be true
55
+ end
56
+
57
+ it "returns false if no chapter has been started" do
58
+ allow_any_instance_of(Daigaku::Chapter).to receive(:started?) { false }
59
+ expect(subject.started?).to be false
60
+ end
61
+ end
62
+
63
+ describe "#mastered?" do
64
+ it "returns true if all chapters have been mastered" do
65
+ allow_any_instance_of(Daigaku::Chapter).to receive(:mastered?) { true }
66
+ expect(subject.mastered?).to be true
67
+ end
68
+
69
+ it "returns false unless all chapters have been mastered" do
70
+ allow_any_instance_of(Daigaku::Chapter).to receive(:mastered?) { false }
71
+ allow(subject.chapters.first).to receive(:mastered?) { true }
72
+ expect(subject.mastered?).to be false
73
+ end
74
+ end
75
+ end