dentaku 2.0.3 → 2.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/CHANGELOG.md +6 -0
- data/lib/dentaku.rb +13 -0
- data/lib/dentaku/ast/arithmetic.rb +25 -14
- data/lib/dentaku/ast/identifier.rb +8 -2
- data/lib/dentaku/bulk_expression_solver.rb +28 -10
- data/lib/dentaku/calculator.rb +8 -3
- data/lib/dentaku/exceptions.rb +1 -0
- data/lib/dentaku/token_scanner.rb +26 -14
- data/lib/dentaku/version.rb +1 -1
- data/spec/bulk_expression_solver_spec.rb +9 -8
- data/spec/calculator_spec.rb +24 -3
- data/spec/exceptions_spec.rb +9 -0
- data/spec/token_scanner_spec.rb +12 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 568297af6c05c99fdd87b6a2d70cd78b4d1f5828
|
4
|
+
data.tar.gz: 9e7668dd3998237a5fe6aaf50370dadfb6941bee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 51685ec73f241afaed1bf66961b55837792100a0ecf06c2d5b283eca227fdb3129c76045fe676b8416fa40d54e11d1ef0da8ef298c504faeace2f77caf168955
|
7
|
+
data.tar.gz: a9a6914b90950b7616e3c82c704ab58921d0107045a3f440f5de2848acbf056ee95fefd8f2f63b663cb33f71f72f3733d94f13b7dd1f0895c1285f5bcab9181c
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v2.0.4] 2015-09-03
|
4
|
+
- fix BigDecimal conversion bug
|
5
|
+
- add caching for bulk expression solving dependency order
|
6
|
+
- allow for custom configuration for token scanners
|
7
|
+
|
3
8
|
## [v2.0.3] 2015-08-25
|
4
9
|
- bug fixes
|
5
10
|
- performance enhancements
|
@@ -80,6 +85,7 @@
|
|
80
85
|
## [v0.1.0] 2012-01-20
|
81
86
|
- initial release
|
82
87
|
|
88
|
+
[v2.0.4]: https://github.com/rubysolo/dentaku/compare/v2.0.3...v2.0.4
|
83
89
|
[v2.0.3]: https://github.com/rubysolo/dentaku/compare/v2.0.1...v2.0.3
|
84
90
|
[v2.0.1]: https://github.com/rubysolo/dentaku/compare/v2.0.0...v2.0.1
|
85
91
|
[v2.0.0]: https://github.com/rubysolo/dentaku/compare/v1.2.6...v2.0.0
|
data/lib/dentaku.rb
CHANGED
@@ -7,6 +7,11 @@ module Dentaku
|
|
7
7
|
calculator.evaluate(expression, data)
|
8
8
|
end
|
9
9
|
|
10
|
+
def self.enable_caching!
|
11
|
+
enable_ast_cache!
|
12
|
+
enable_dependency_order_cache!
|
13
|
+
end
|
14
|
+
|
10
15
|
def self.enable_ast_cache!
|
11
16
|
@enable_ast_caching = true
|
12
17
|
end
|
@@ -15,6 +20,14 @@ module Dentaku
|
|
15
20
|
@enable_ast_caching
|
16
21
|
end
|
17
22
|
|
23
|
+
def self.enable_dependency_order_cache!
|
24
|
+
@enable_dependency_order_caching = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.cache_dependency_order?
|
28
|
+
@enable_dependency_order_caching
|
29
|
+
end
|
30
|
+
|
18
31
|
private
|
19
32
|
|
20
33
|
def self.calculator
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative './operation'
|
2
2
|
require 'bigdecimal'
|
3
|
+
require 'bigdecimal/util'
|
3
4
|
|
4
5
|
module Dentaku
|
5
6
|
module AST
|
@@ -13,16 +14,28 @@ module Dentaku
|
|
13
14
|
:numeric
|
14
15
|
end
|
15
16
|
|
17
|
+
def value(context={})
|
18
|
+
l = cast(left.value(context))
|
19
|
+
r = cast(right.value(context))
|
20
|
+
l.public_send(operator, r)
|
21
|
+
end
|
22
|
+
|
16
23
|
private
|
17
24
|
|
25
|
+
def cast(value, prefer_integer=true)
|
26
|
+
v = BigDecimal.new(value, Float::DIG+1)
|
27
|
+
v = v.to_i if prefer_integer && v.frac.zero?
|
28
|
+
v
|
29
|
+
end
|
30
|
+
|
18
31
|
def valid_node?(node)
|
19
32
|
node.is_a?(Identifier) || node.type == :numeric
|
20
33
|
end
|
21
34
|
end
|
22
35
|
|
23
36
|
class Addition < Arithmetic
|
24
|
-
def
|
25
|
-
|
37
|
+
def operator
|
38
|
+
:+
|
26
39
|
end
|
27
40
|
|
28
41
|
def self.precedence
|
@@ -31,8 +44,8 @@ module Dentaku
|
|
31
44
|
end
|
32
45
|
|
33
46
|
class Subtraction < Arithmetic
|
34
|
-
def
|
35
|
-
|
47
|
+
def operator
|
48
|
+
:-
|
36
49
|
end
|
37
50
|
|
38
51
|
def self.precedence
|
@@ -41,8 +54,8 @@ module Dentaku
|
|
41
54
|
end
|
42
55
|
|
43
56
|
class Multiplication < Arithmetic
|
44
|
-
def
|
45
|
-
|
57
|
+
def operator
|
58
|
+
:*
|
46
59
|
end
|
47
60
|
|
48
61
|
def self.precedence
|
@@ -52,12 +65,10 @@ module Dentaku
|
|
52
65
|
|
53
66
|
class Division < Arithmetic
|
54
67
|
def value(context={})
|
55
|
-
r =
|
68
|
+
r = cast(right.value(context), false)
|
56
69
|
raise ZeroDivisionError if r.zero?
|
57
70
|
|
58
|
-
|
59
|
-
v = v.to_i if v.frac.zero?
|
60
|
-
v
|
71
|
+
cast(cast(left.value(context)) / r)
|
61
72
|
end
|
62
73
|
|
63
74
|
def self.precedence
|
@@ -66,8 +77,8 @@ module Dentaku
|
|
66
77
|
end
|
67
78
|
|
68
79
|
class Modulo < Arithmetic
|
69
|
-
def
|
70
|
-
|
80
|
+
def operator
|
81
|
+
:%
|
71
82
|
end
|
72
83
|
|
73
84
|
def self.precedence
|
@@ -76,8 +87,8 @@ module Dentaku
|
|
76
87
|
end
|
77
88
|
|
78
89
|
class Exponentiation < Arithmetic
|
79
|
-
def
|
80
|
-
|
90
|
+
def operator
|
91
|
+
:**
|
81
92
|
end
|
82
93
|
|
83
94
|
def self.precedence
|
@@ -13,7 +13,7 @@ module Dentaku
|
|
13
13
|
v = context[identifier]
|
14
14
|
case v
|
15
15
|
when Node
|
16
|
-
v.value
|
16
|
+
v.value(context)
|
17
17
|
when NilClass
|
18
18
|
raise UnboundVariableError.new([identifier])
|
19
19
|
else
|
@@ -22,7 +22,13 @@ module Dentaku
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def dependencies(context={})
|
25
|
-
context.has_key?(identifier) ? [] : [identifier]
|
25
|
+
context.has_key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def dependencies_of(node)
|
31
|
+
node.respond_to?(:dependencies) ? node.dependencies : []
|
26
32
|
end
|
27
33
|
end
|
28
34
|
end
|
@@ -6,9 +6,9 @@ require 'dentaku/tokenizer'
|
|
6
6
|
|
7
7
|
module Dentaku
|
8
8
|
class BulkExpressionSolver
|
9
|
-
def initialize(expression_hash,
|
9
|
+
def initialize(expression_hash, calculator)
|
10
10
|
self.expression_hash = expression_hash
|
11
|
-
self.calculator =
|
11
|
+
self.calculator = calculator
|
12
12
|
end
|
13
13
|
|
14
14
|
def solve!
|
@@ -26,6 +26,10 @@ module Dentaku
|
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
+
def self.dependency_cache
|
30
|
+
@dep_cache ||= {}
|
31
|
+
end
|
32
|
+
|
29
33
|
attr_accessor :expression_hash, :calculator
|
30
34
|
|
31
35
|
def return_undefined_handler
|
@@ -39,28 +43,42 @@ module Dentaku
|
|
39
43
|
def load_results(&block)
|
40
44
|
variables_in_resolve_order.each_with_object({}) do |var_name, r|
|
41
45
|
begin
|
42
|
-
r[var_name] = calculator.memory[var_name] ||
|
46
|
+
r[var_name] = calculator.memory[var_name] ||
|
47
|
+
evaluate!(expressions[var_name], expressions.merge(r))
|
43
48
|
rescue Dentaku::UnboundVariableError, ZeroDivisionError => ex
|
44
49
|
r[var_name] = block.call(ex)
|
45
50
|
end
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
49
|
-
def dependencies(expression)
|
50
|
-
Parser.new(Tokenizer.new.tokenize(expression)).parse.dependencies
|
51
|
-
end
|
52
|
-
|
53
54
|
def expressions
|
54
55
|
@expressions ||= Hash[expression_hash.map { |k,v| [k.to_s, v] }]
|
55
56
|
end
|
56
57
|
|
57
58
|
def expression_dependencies
|
58
|
-
Hash[expressions.map { |var, expr| [var, dependencies(expr)] }]
|
59
|
+
Hash[expressions.map { |var, expr| [var, calculator.dependencies(expr)] }].tap do |d|
|
60
|
+
d.values.each do |deps|
|
61
|
+
unresolved = deps.reject { |ud| d.has_key?(ud) }
|
62
|
+
unresolved.each { |u| add_dependencies(d, u) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_dependencies(current_dependencies, variable)
|
68
|
+
node = calculator.memory[variable]
|
69
|
+
if node.respond_to?(:dependencies)
|
70
|
+
current_dependencies[variable] = node.dependencies
|
71
|
+
node.dependencies.each { |d| add_dependencies(current_dependencies, d) }
|
72
|
+
end
|
59
73
|
end
|
60
74
|
|
61
75
|
def variables_in_resolve_order
|
62
|
-
|
63
|
-
|
76
|
+
cache_key = expressions.keys.map(&:to_s).sort.join("|")
|
77
|
+
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
78
|
+
DependencyResolver.find_resolve_order(expression_dependencies).tap do |d|
|
79
|
+
self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
|
80
|
+
end
|
81
|
+
}
|
64
82
|
end
|
65
83
|
|
66
84
|
def evaluate!(expression, results)
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'dentaku'
|
1
2
|
require 'dentaku/bulk_expression_solver'
|
2
3
|
require 'dentaku/exceptions'
|
3
4
|
require 'dentaku/token'
|
@@ -31,7 +32,7 @@ module Dentaku
|
|
31
32
|
end
|
32
33
|
|
33
34
|
def evaluate!(expression, data={})
|
34
|
-
|
35
|
+
store(data) do
|
35
36
|
node = expression
|
36
37
|
node = ast(node) unless node.is_a?(AST::Node)
|
37
38
|
node.value(memory)
|
@@ -39,11 +40,11 @@ module Dentaku
|
|
39
40
|
end
|
40
41
|
|
41
42
|
def solve!(expression_hash)
|
42
|
-
BulkExpressionSolver.new(expression_hash,
|
43
|
+
BulkExpressionSolver.new(expression_hash, self).solve!
|
43
44
|
end
|
44
45
|
|
45
46
|
def solve(expression_hash, &block)
|
46
|
-
BulkExpressionSolver.new(expression_hash,
|
47
|
+
BulkExpressionSolver.new(expression_hash, self).solve(&block)
|
47
48
|
end
|
48
49
|
|
49
50
|
def dependencies(expression)
|
@@ -79,6 +80,10 @@ module Dentaku
|
|
79
80
|
end
|
80
81
|
alias_method :bind, :store
|
81
82
|
|
83
|
+
def store_formula(key, formula)
|
84
|
+
store(key, ast(formula))
|
85
|
+
end
|
86
|
+
|
82
87
|
def clear
|
83
88
|
@memory = {}
|
84
89
|
end
|
data/lib/dentaku/exceptions.rb
CHANGED
@@ -24,23 +24,35 @@ module Dentaku
|
|
24
24
|
end
|
25
25
|
|
26
26
|
class << self
|
27
|
-
def
|
28
|
-
|
29
|
-
whitespace,
|
30
|
-
numeric,
|
31
|
-
double_quoted_string,
|
32
|
-
single_quoted_string,
|
33
|
-
negate,
|
34
|
-
operator,
|
35
|
-
grouping,
|
36
|
-
comparator,
|
37
|
-
combinator,
|
38
|
-
boolean,
|
39
|
-
function,
|
40
|
-
identifier
|
27
|
+
def available_scanners
|
28
|
+
[
|
29
|
+
:whitespace,
|
30
|
+
:numeric,
|
31
|
+
:double_quoted_string,
|
32
|
+
:single_quoted_string,
|
33
|
+
:negate,
|
34
|
+
:operator,
|
35
|
+
:grouping,
|
36
|
+
:comparator,
|
37
|
+
:combinator,
|
38
|
+
:boolean,
|
39
|
+
:function,
|
40
|
+
:identifier
|
41
41
|
]
|
42
42
|
end
|
43
43
|
|
44
|
+
def scanners=(token_scanners)
|
45
|
+
@scanners = (token_scanners & available_scanners).map { |scanner|
|
46
|
+
self.send(scanner)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def scanners
|
51
|
+
@scanners ||= available_scanners.map { |scanner|
|
52
|
+
self.send(scanner)
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
44
56
|
def whitespace
|
45
57
|
new(:whitespace, '\s+')
|
46
58
|
end
|
data/lib/dentaku/version.rb
CHANGED
@@ -2,6 +2,8 @@ require 'spec_helper'
|
|
2
2
|
require 'dentaku/bulk_expression_solver'
|
3
3
|
|
4
4
|
RSpec.describe Dentaku::BulkExpressionSolver do
|
5
|
+
let(:calculator) { Dentaku::Calculator.new }
|
6
|
+
|
5
7
|
describe "#solve!" do
|
6
8
|
it "evaluates properly with variables, even if some in memory" do
|
7
9
|
expressions = {
|
@@ -9,8 +11,7 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
9
11
|
weekly_apple_budget: "apples * 7",
|
10
12
|
pear: "1"
|
11
13
|
}
|
12
|
-
|
13
|
-
solver = described_class.new(expressions, memory)
|
14
|
+
solver = described_class.new(expressions, calculator.store(apples: 3))
|
14
15
|
expect(solver.solve!)
|
15
16
|
.to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
16
17
|
end
|
@@ -18,20 +19,20 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
18
19
|
it "lets you know if a variable is unbound" do
|
19
20
|
expressions = {more_apples: "apples + 1"}
|
20
21
|
expect {
|
21
|
-
described_class.new(expressions,
|
22
|
+
described_class.new(expressions, calculator).solve!
|
22
23
|
}.to raise_error(Dentaku::UnboundVariableError)
|
23
24
|
end
|
24
25
|
|
25
26
|
it "lets you know if the result is a div/0 error" do
|
26
27
|
expressions = {more_apples: "1/0"}
|
27
28
|
expect {
|
28
|
-
described_class.new(expressions,
|
29
|
+
described_class.new(expressions, calculator).solve!
|
29
30
|
}.to raise_error(ZeroDivisionError)
|
30
31
|
end
|
31
32
|
|
32
33
|
it "does not require keys to be parseable" do
|
33
34
|
expressions = { "the value of x, incremented" => "x + 1" }
|
34
|
-
solver = described_class.new(expressions, "x" => 3)
|
35
|
+
solver = described_class.new(expressions, calculator.store("x" => 3))
|
35
36
|
expect(solver.solve!).to eq({ "the value of x, incremented" => 4 })
|
36
37
|
end
|
37
38
|
end
|
@@ -39,19 +40,19 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
39
40
|
describe "#solve" do
|
40
41
|
it "returns :undefined when variables are unbound" do
|
41
42
|
expressions = {more_apples: "apples + 1"}
|
42
|
-
expect(described_class.new(expressions,
|
43
|
+
expect(described_class.new(expressions, calculator).solve)
|
43
44
|
.to eq(more_apples: :undefined)
|
44
45
|
end
|
45
46
|
|
46
47
|
it "allows passing in a custom value to an error handler when a variable is unbound" do
|
47
48
|
expressions = {more_apples: "apples + 1"}
|
48
|
-
expect(described_class.new(expressions,
|
49
|
+
expect(described_class.new(expressions, calculator).solve { :foo })
|
49
50
|
.to eq(more_apples: :foo)
|
50
51
|
end
|
51
52
|
|
52
53
|
it "allows passing in a custom value to an error handler when there is a div/0 error" do
|
53
54
|
expressions = {more_apples: "1/0"}
|
54
|
-
expect(described_class.new(expressions,
|
55
|
+
expect(described_class.new(expressions, calculator).solve { :foo })
|
55
56
|
.to eq(more_apples: :foo)
|
56
57
|
end
|
57
58
|
end
|
data/spec/calculator_spec.rb
CHANGED
@@ -29,6 +29,8 @@ describe Dentaku::Calculator do
|
|
29
29
|
expect(calculator.evaluate('3 + 0 / -3')).to eq(3)
|
30
30
|
expect(calculator.evaluate('15 % 8')).to eq(7)
|
31
31
|
expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
|
32
|
+
expect(calculator.evaluate('0.253/0.253')).to eq(1)
|
33
|
+
expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
|
32
34
|
end
|
33
35
|
|
34
36
|
describe 'memory' do
|
@@ -51,6 +53,11 @@ describe Dentaku::Calculator do
|
|
51
53
|
expect(calculator.evaluate!('first')).to eq 1
|
52
54
|
expect(calculator.evaluate!('second')).to eq 2
|
53
55
|
end
|
56
|
+
|
57
|
+
it 'stores formulas' do
|
58
|
+
calculator.store_formula('area', 'length * width')
|
59
|
+
expect(calculator.evaluate!('area', length: 5, width: 5)).to eq 25
|
60
|
+
end
|
54
61
|
end
|
55
62
|
|
56
63
|
describe 'dependencies' do
|
@@ -68,15 +75,15 @@ describe Dentaku::Calculator do
|
|
68
75
|
expect(with_memory.solve!(
|
69
76
|
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
70
77
|
weekly_apple_budget: "apples * 7",
|
71
|
-
pear:
|
78
|
+
pear: "1"
|
72
79
|
)).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
73
80
|
end
|
74
81
|
|
75
82
|
it "preserves hash keys" do
|
76
83
|
expect(calculator.solve!(
|
77
84
|
'meaning_of_life' => 'age + kids',
|
78
|
-
'age'
|
79
|
-
'kids'
|
85
|
+
'age' => 40,
|
86
|
+
'kids' => 2
|
80
87
|
)).to eq('age' => 40, 'kids' => 2, 'meaning_of_life' => 42)
|
81
88
|
end
|
82
89
|
|
@@ -96,6 +103,20 @@ describe Dentaku::Calculator do
|
|
96
103
|
calculator.solve!(more_apples: "apples + 1")
|
97
104
|
}.to raise_error(Dentaku::UnboundVariableError)
|
98
105
|
end
|
106
|
+
|
107
|
+
it 'can reference stored formulas' do
|
108
|
+
calculator.store_formula("base_area", "length * width")
|
109
|
+
calculator.store_formula("volume", "base_area * height")
|
110
|
+
|
111
|
+
result = calculator.solve!(
|
112
|
+
weight: "volume * 5.432",
|
113
|
+
height: "3",
|
114
|
+
length: "2",
|
115
|
+
width: "length * 2",
|
116
|
+
)
|
117
|
+
|
118
|
+
expect(result[:weight]).to eq 130.368
|
119
|
+
end
|
99
120
|
end
|
100
121
|
|
101
122
|
describe 'solve' do
|
data/spec/token_scanner_spec.rb
CHANGED
@@ -25,4 +25,16 @@ describe Dentaku::TokenScanner do
|
|
25
25
|
it 'returns a list of all configured scanners' do
|
26
26
|
expect(described_class.scanners.length).to eq 12
|
27
27
|
end
|
28
|
+
|
29
|
+
it 'allows customizing available scanners' do
|
30
|
+
described_class.scanners = [:whitespace, :numeric]
|
31
|
+
expect(described_class.scanners.length).to eq 2
|
32
|
+
described_class.scanners = described_class.available_scanners
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'ignores invalid scanners' do
|
36
|
+
described_class.scanners = [:whitespace, :numeric, :fake]
|
37
|
+
expect(described_class.scanners.length).to eq 2
|
38
|
+
described_class.scanners = described_class.available_scanners
|
39
|
+
end
|
28
40
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -113,6 +113,7 @@ files:
|
|
113
113
|
- spec/bulk_expression_solver_spec.rb
|
114
114
|
- spec/calculator_spec.rb
|
115
115
|
- spec/dentaku_spec.rb
|
116
|
+
- spec/exceptions_spec.rb
|
116
117
|
- spec/external_function_spec.rb
|
117
118
|
- spec/parser_spec.rb
|
118
119
|
- spec/spec_helper.rb
|
@@ -155,6 +156,7 @@ test_files:
|
|
155
156
|
- spec/bulk_expression_solver_spec.rb
|
156
157
|
- spec/calculator_spec.rb
|
157
158
|
- spec/dentaku_spec.rb
|
159
|
+
- spec/exceptions_spec.rb
|
158
160
|
- spec/external_function_spec.rb
|
159
161
|
- spec/parser_spec.rb
|
160
162
|
- spec/spec_helper.rb
|