daigaku 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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