Pickaxe 0.1.3 → 0.2.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.
- data/README.markdown +2 -0
- data/bin/pickaxe +1 -1
- data/lib/pickaxe.rb +25 -0
- data/lib/pickaxe/color.rb +54 -0
- data/lib/pickaxe/extensions.rb +27 -0
- data/lib/pickaxe/main.rb +112 -0
- data/lib/pickaxe/shell.rb +16 -0
- data/lib/pickaxe/test.rb +128 -0
- metadata +15 -6
data/README.markdown
CHANGED
data/bin/pickaxe
CHANGED
data/lib/pickaxe.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler"
|
3
|
+
|
4
|
+
Bundler.setup(:default)
|
5
|
+
|
6
|
+
require 'active_support/all'
|
7
|
+
|
8
|
+
module Pickaxe
|
9
|
+
VERSION = "0.2.0"
|
10
|
+
|
11
|
+
class PickaxeError < StandardError
|
12
|
+
attr_reader :status_code
|
13
|
+
def self.status_code(code = nil)
|
14
|
+
define_method(:status_code) { code }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
autoload :Shell, 'pickaxe/shell'
|
19
|
+
autoload :Color, 'pickaxe/color'
|
20
|
+
autoload :Main, 'pickaxe/main'
|
21
|
+
autoload :Test, 'pickaxe/test'
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'pickaxe/extensions'
|
25
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Pickaxe
|
2
|
+
# Extracted from https://github.com/wycats/thor/blob/master/lib/thor/shell/color.rb
|
3
|
+
module Color
|
4
|
+
# Embed in a String to clear all previous ANSI sequences.
|
5
|
+
CLEAR = "\e[0m"
|
6
|
+
# The start of an ANSI bold sequence.
|
7
|
+
BOLD = "\e[1m"
|
8
|
+
|
9
|
+
# Set the terminal's foreground ANSI color to black.
|
10
|
+
BLACK = "\e[30m"
|
11
|
+
# Set the terminal's foreground ANSI color to red.
|
12
|
+
RED = "\e[31m"
|
13
|
+
# Set the terminal's foreground ANSI color to green.
|
14
|
+
GREEN = "\e[32m"
|
15
|
+
# Set the terminal's foreground ANSI color to yellow.
|
16
|
+
YELLOW = "\e[33m"
|
17
|
+
# Set the terminal's foreground ANSI color to blue.
|
18
|
+
BLUE = "\e[34m"
|
19
|
+
# Set the terminal's foreground ANSI color to magenta.
|
20
|
+
MAGENTA = "\e[35m"
|
21
|
+
# Set the terminal's foreground ANSI color to cyan.
|
22
|
+
CYAN = "\e[36m"
|
23
|
+
# Set the terminal's foreground ANSI color to white.
|
24
|
+
WHITE = "\e[37m"
|
25
|
+
|
26
|
+
# Set the terminal's background ANSI color to black.
|
27
|
+
ON_BLACK = "\e[40m"
|
28
|
+
# Set the terminal's background ANSI color to red.
|
29
|
+
ON_RED = "\e[41m"
|
30
|
+
# Set the terminal's background ANSI color to green.
|
31
|
+
ON_GREEN = "\e[42m"
|
32
|
+
# Set the terminal's background ANSI color to yellow.
|
33
|
+
ON_YELLOW = "\e[43m"
|
34
|
+
# Set the terminal's background ANSI color to blue.
|
35
|
+
ON_BLUE = "\e[44m"
|
36
|
+
# Set the terminal's background ANSI color to magenta.
|
37
|
+
ON_MAGENTA = "\e[45m"
|
38
|
+
# Set the terminal's background ANSI color to cyan.
|
39
|
+
ON_CYAN = "\e[46m"
|
40
|
+
# Set the terminal's background ANSI color to white.
|
41
|
+
ON_WHITE = "\e[47m"
|
42
|
+
|
43
|
+
# Set color by using a string or one of the defined constants. If a third
|
44
|
+
# option is set to true, it also adds bold to the string. This is based
|
45
|
+
# on Highline implementation and it automatically appends CLEAR to the end
|
46
|
+
# of the returned String.
|
47
|
+
#
|
48
|
+
def self.set_color(string, color, bold=false)
|
49
|
+
color = self.const_get(color.to_s.upcase) if color.is_a?(Symbol)
|
50
|
+
bold = bold ? BOLD : ""
|
51
|
+
"#{bold}#{color}#{string}#{CLEAR}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class String
|
2
|
+
def word_wrap(*args)
|
3
|
+
options = args.extract_options!
|
4
|
+
unless args.blank?
|
5
|
+
options[:line_width] = args[0] || Pickaxe::Shell.dynamic_width || 80
|
6
|
+
end
|
7
|
+
options.reverse_merge!(:line_width => Pickaxe::Shell.dynamic_width || 80)
|
8
|
+
|
9
|
+
self.split("\n").collect do |line|
|
10
|
+
line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
|
11
|
+
end * "\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
def color(name, bold=false)
|
15
|
+
Pickaxe::Color.set_color(self, name, bold)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Fixnum
|
20
|
+
def to_duration
|
21
|
+
s = self
|
22
|
+
m = s / 60
|
23
|
+
s -= m * 60
|
24
|
+
|
25
|
+
[m, s].compact.zip(["m", "s"]).collect(&:join).join(" ")
|
26
|
+
end
|
27
|
+
end
|
data/lib/pickaxe/main.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module Pickaxe
|
2
|
+
class Main
|
3
|
+
class NoTests < PickaxeError; status_code(1) ; end
|
4
|
+
|
5
|
+
def initialize(paths, options = {})
|
6
|
+
raise NoTests, "no tests to run" if paths.empty?
|
7
|
+
@test = Test.new(options, *paths)
|
8
|
+
@questions = @test.shuffled_questions
|
9
|
+
@answers = Hash.new([])
|
10
|
+
|
11
|
+
@started_at = Time.now
|
12
|
+
@current_index = 0
|
13
|
+
while @current_index < @questions.length do
|
14
|
+
@question = @questions[@current_index]
|
15
|
+
|
16
|
+
puts "#{@current_index+1} / #{@questions.length}\t\tFrom: #{@question.file}\t\tTime spent: #{spent?}"
|
17
|
+
puts @question.answered(@answers[@question])
|
18
|
+
|
19
|
+
until (line = prompt?).nil? or command(line)
|
20
|
+
# empty
|
21
|
+
end
|
22
|
+
|
23
|
+
break if puts or line.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
statistics!
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Available commands
|
31
|
+
# ^ question jumps to given question
|
32
|
+
# <+ moves back one question
|
33
|
+
# >+ moves forward one question
|
34
|
+
# ! a [ b ...] answers the question and forces to show correct answers
|
35
|
+
# a [ b ...] answers the question
|
36
|
+
# ? shows help
|
37
|
+
#
|
38
|
+
def command(line)
|
39
|
+
case line
|
40
|
+
when /^\s*@\s*(.+)/ then # @ question
|
41
|
+
@current_index = Integer($1) -1
|
42
|
+
true
|
43
|
+
when /<+/ then
|
44
|
+
if @current_index > 0
|
45
|
+
@current_index -= 1
|
46
|
+
true
|
47
|
+
else
|
48
|
+
error "You are at first question"
|
49
|
+
end
|
50
|
+
when />+/ then
|
51
|
+
if @current_index < (@questions.length - 1)
|
52
|
+
@current_index += 1
|
53
|
+
true
|
54
|
+
else
|
55
|
+
error "You are at last question"
|
56
|
+
end
|
57
|
+
when "\n" then
|
58
|
+
@current_index += 1
|
59
|
+
true
|
60
|
+
when /^\s*!\s*(.+)/ then
|
61
|
+
raise NotImplementedError
|
62
|
+
when /\?/ then
|
63
|
+
puts <<END_OF_HELP
|
64
|
+
|
65
|
+
Available commands (whitespace does not matter):
|
66
|
+
@ question jumps to given question
|
67
|
+
< moves back one question
|
68
|
+
> moves forward one question
|
69
|
+
a [ b ...] answers the question
|
70
|
+
! a [ b ...] answers the question and forces reveal correct answers
|
71
|
+
? shows help
|
72
|
+
|
73
|
+
END_OF_HELP
|
74
|
+
false
|
75
|
+
else
|
76
|
+
@answers[@question] = line.split(/\s+/).collect(&:strip)
|
77
|
+
@current_index += 1
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def statistics!
|
83
|
+
@stats = @test.statistics!(@answers)
|
84
|
+
|
85
|
+
puts
|
86
|
+
puts "Time: #{spent?}"
|
87
|
+
puts "All: #{@questions.length}"
|
88
|
+
stat :correct, :green
|
89
|
+
stat :unanswered, :yellow
|
90
|
+
stat :incorrect, :red
|
91
|
+
end
|
92
|
+
|
93
|
+
def error(msg)
|
94
|
+
$stderr.puts(("! " + msg).color(:red))
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def prompt?(p = "? ")
|
99
|
+
print p
|
100
|
+
$stdin.gets
|
101
|
+
end
|
102
|
+
|
103
|
+
def spent?
|
104
|
+
(Time.now - @started_at).to_i.to_duration
|
105
|
+
end
|
106
|
+
protected
|
107
|
+
def stat(name, color)
|
108
|
+
value = @stats[name.to_s.downcase.to_sym]
|
109
|
+
puts "#{name.to_s.capitalize}: #{value} (#{value/@questions.length.to_f * 100}%%)".color(color)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Pickaxe
|
2
|
+
module Shell
|
3
|
+
# Extracted from https://github.com/wycats/thor/blob/master/lib/thor/shell/basic.rb
|
4
|
+
def self.dynamic_width
|
5
|
+
(dynamic_width_stty.nonzero? || dynamic_width_tput)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.dynamic_width_stty
|
9
|
+
%x{stty size 2>/dev/null}.split[1].to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.dynamic_width_tput
|
13
|
+
%x{tput cols 2>/dev/null}.to_i
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/pickaxe/test.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
module Pickaxe
|
2
|
+
#
|
3
|
+
# Test is a file in which questions are separated by a blank line.
|
4
|
+
# Each question has content (first line), and answers remaining lines.
|
5
|
+
# Answers are listed one per line which starts with optional >> (means answer
|
6
|
+
# is correct), followed by index in parenthesis (index) and followed by text.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# 1. To be or not to be?
|
11
|
+
# (a) To be.
|
12
|
+
# (b) Not to be.
|
13
|
+
# >> (c) I do not know.
|
14
|
+
#
|
15
|
+
class Test
|
16
|
+
attr_reader :questions
|
17
|
+
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
# Ruby-comments and C-comments
|
21
|
+
COMMENTS_RE = /^#.*|^\/\/.*/
|
22
|
+
|
23
|
+
class PathError < PickaxeError; status_code(1) ; end
|
24
|
+
class MissingAnswers < PickaxeError; status_code(2) ; end
|
25
|
+
class BadAnswer < PickaxeError; status_code(3) ; end
|
26
|
+
|
27
|
+
def initialize(options, *files)
|
28
|
+
@options = options
|
29
|
+
@files = files.collect do |file_or_directory|
|
30
|
+
raise PathError, "file or directory '#{file_or_directory}' does not exist" unless File.exist?(file_or_directory)
|
31
|
+
if File.file?(file_or_directory)
|
32
|
+
file_or_directory
|
33
|
+
else
|
34
|
+
Dir.glob("#{file_or_directory}/*.#{@options[:extension] || "txt"}")
|
35
|
+
end
|
36
|
+
end.flatten
|
37
|
+
|
38
|
+
@questions = []
|
39
|
+
@files.each do |file|
|
40
|
+
File.open(file) do |f|
|
41
|
+
lines = f.readlines.collect(&:strip)
|
42
|
+
lines = lines.reject {|line| line =~ COMMENTS_RE }
|
43
|
+
lines.split("").reject(&:blank?).each do |question|
|
44
|
+
@questions.push(Question.parse(file, question))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Yields questions randomly
|
51
|
+
def each(&block)
|
52
|
+
shuffled_questions.each(&block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def shuffled_questions
|
56
|
+
questions = if @options[:sorted]
|
57
|
+
@questions
|
58
|
+
else
|
59
|
+
@questions.shuffle
|
60
|
+
end
|
61
|
+
|
62
|
+
@selected = if @options[:select]
|
63
|
+
questions[0...(@options[:select])]
|
64
|
+
else
|
65
|
+
questions
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def selected
|
70
|
+
@selected ||= @questions
|
71
|
+
end
|
72
|
+
|
73
|
+
def statistics!(answers)
|
74
|
+
Hash.new(0).tap do |statistics|
|
75
|
+
selected.each do |question|
|
76
|
+
given = answers[question]
|
77
|
+
if question.correct?(given)
|
78
|
+
statistics[:correct] += 1
|
79
|
+
elsif given.blank?
|
80
|
+
statistics[:unanswered] += 1
|
81
|
+
else
|
82
|
+
statistics[:incorrect] += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Question < Struct.new(:file, :content, :answers)
|
90
|
+
def self.parse(file, answers)
|
91
|
+
content = answers.shift
|
92
|
+
raise MissingAnswers, "question '#{content.truncate(20)}' has no answers'" if answers.blank?
|
93
|
+
Question.new(file, content, answers.collect {|answer| Answer.parse(answer) })
|
94
|
+
end
|
95
|
+
|
96
|
+
def answered(indices)
|
97
|
+
"#{self.content.word_wrap}\n\n" + self.answers.collect do |answer|
|
98
|
+
selected = indices.include?(answer.index)
|
99
|
+
line = (selected ? ">> " : " ") + answer.to_s
|
100
|
+
unless indices.blank?
|
101
|
+
if selected and answer.correctness
|
102
|
+
line.color(:green)
|
103
|
+
elsif not selected and answer.correctness
|
104
|
+
line.color(:yellow)
|
105
|
+
elsif selected and not answer.correctness
|
106
|
+
line.color(:red)
|
107
|
+
end
|
108
|
+
end || line
|
109
|
+
end.join("\n") + "\n\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
def correct?(given)
|
113
|
+
given.sort == answers.select(&:correctness).collect(&:index).sort
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Answer < Struct.new(:content, :index, :correctness)
|
118
|
+
RE = /^\s*(>>)?(\?\?)?\s*\((\w+)\)\s*(.+)$/
|
119
|
+
def self.parse(line)
|
120
|
+
raise BadAnswer, "'#{line.truncate(20)}' does not look like answer" if (m = RE.match(line)).nil?
|
121
|
+
Answer.new(m[m.size-1].strip, m[m.size-2].strip, m[1] == ">>")
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
"(#{self.index}) #{self.content}".word_wrap
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: Pickaxe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Dawid Fatyga
|
@@ -62,7 +62,10 @@ dependencies:
|
|
62
62
|
name: i18n
|
63
63
|
prerelease: false
|
64
64
|
type: :runtime
|
65
|
-
description:
|
65
|
+
description: |
|
66
|
+
Pickaxe provides a simple way to load, solve and rate tests (bundle of questions)
|
67
|
+
written in simple text format.
|
68
|
+
|
66
69
|
email: dawid.fatyga@gmail.com
|
67
70
|
executables:
|
68
71
|
- pickaxe
|
@@ -71,8 +74,14 @@ extensions: []
|
|
71
74
|
extra_rdoc_files:
|
72
75
|
- README.markdown
|
73
76
|
files:
|
74
|
-
-
|
77
|
+
- lib/pickaxe.rb
|
78
|
+
- lib/pickaxe/test.rb
|
79
|
+
- lib/pickaxe/color.rb
|
80
|
+
- lib/pickaxe/extensions.rb
|
81
|
+
- lib/pickaxe/shell.rb
|
82
|
+
- lib/pickaxe/main.rb
|
75
83
|
- bin/pickaxe
|
84
|
+
- README.markdown
|
76
85
|
has_rdoc: true
|
77
86
|
homepage: https://github.com/dejw/pickaxe
|
78
87
|
licenses: []
|