delta_test 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +4 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +165 -0
  8. data/Rakefile +17 -0
  9. data/bin/delta_test +12 -0
  10. data/circle.yml +12 -0
  11. data/delta_test.gemspec +30 -0
  12. data/lib/delta_test/analyzer.rb +47 -0
  13. data/lib/delta_test/cli.rb +224 -0
  14. data/lib/delta_test/configuration.rb +173 -0
  15. data/lib/delta_test/dependencies_table.rb +83 -0
  16. data/lib/delta_test/errors.rb +55 -0
  17. data/lib/delta_test/generator.rb +101 -0
  18. data/lib/delta_test/git.rb +88 -0
  19. data/lib/delta_test/related_spec_list.rb +64 -0
  20. data/lib/delta_test/spec_helpers.rb +42 -0
  21. data/lib/delta_test/utils.rb +93 -0
  22. data/lib/delta_test/version.rb +9 -0
  23. data/lib/delta_test.rb +47 -0
  24. data/spec/fixtures/sample/alpha.rb +19 -0
  25. data/spec/fixtures/sample/beta.rb +15 -0
  26. data/spec/fixtures/sample/gamma.rb +9 -0
  27. data/spec/lib/delta_test/analyzer_spec.rb +126 -0
  28. data/spec/lib/delta_test/cli_spec.rb +422 -0
  29. data/spec/lib/delta_test/configuration_spec.rb +353 -0
  30. data/spec/lib/delta_test/dependencies_table_spec.rb +129 -0
  31. data/spec/lib/delta_test/generator_spec.rb +201 -0
  32. data/spec/lib/delta_test/git_spec.rb +178 -0
  33. data/spec/lib/delta_test/related_spec_list_spec.rb +182 -0
  34. data/spec/lib/delta_test/spec_helpers_spec.rb +72 -0
  35. data/spec/lib/delta_test/utils_spec.rb +244 -0
  36. data/spec/lib/delta_test_spec.rb +119 -0
  37. data/spec/rails/.gitignore +19 -0
  38. data/spec/rails/.rspec +3 -0
  39. data/spec/rails/Gemfile +15 -0
  40. data/spec/rails/Gemfile.lock +163 -0
  41. data/spec/rails/README.rdoc +28 -0
  42. data/spec/rails/Rakefile +6 -0
  43. data/spec/rails/app/controllers/application_controller.rb +5 -0
  44. data/spec/rails/app/controllers/concerns/.keep +0 -0
  45. data/spec/rails/app/helpers/application_helper.rb +2 -0
  46. data/spec/rails/app/mailers/.keep +0 -0
  47. data/spec/rails/app/models/.keep +0 -0
  48. data/spec/rails/app/models/concerns/.keep +0 -0
  49. data/spec/rails/app/views/layouts/application.html.haml +7 -0
  50. data/spec/rails/bin/bundle +3 -0
  51. data/spec/rails/bin/rails +4 -0
  52. data/spec/rails/bin/rake +4 -0
  53. data/spec/rails/bin/setup +29 -0
  54. data/spec/rails/config/application.rb +35 -0
  55. data/spec/rails/config/boot.rb +3 -0
  56. data/spec/rails/config/database.yml +25 -0
  57. data/spec/rails/config/environment.rb +5 -0
  58. data/spec/rails/config/environments/development.rb +41 -0
  59. data/spec/rails/config/environments/production.rb +79 -0
  60. data/spec/rails/config/environments/test.rb +42 -0
  61. data/spec/rails/config/initializers/assets.rb +11 -0
  62. data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
  63. data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
  64. data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
  65. data/spec/rails/config/initializers/inflections.rb +16 -0
  66. data/spec/rails/config/initializers/mime_types.rb +4 -0
  67. data/spec/rails/config/initializers/session_store.rb +3 -0
  68. data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
  69. data/spec/rails/config/locales/en.yml +23 -0
  70. data/spec/rails/config/routes.rb +56 -0
  71. data/spec/rails/config/secrets.yml +22 -0
  72. data/spec/rails/config.ru +4 -0
  73. data/spec/rails/db/seeds.rb +7 -0
  74. data/spec/rails/delta_test.yml +5 -0
  75. data/spec/rails/lib/assets/.keep +0 -0
  76. data/spec/rails/lib/tasks/.keep +0 -0
  77. data/spec/rails/log/.keep +0 -0
  78. data/spec/rails/public/404.html +67 -0
  79. data/spec/rails/public/422.html +67 -0
  80. data/spec/rails/public/500.html +66 -0
  81. data/spec/rails/public/favicon.ico +0 -0
  82. data/spec/rails/public/robots.txt +5 -0
  83. data/spec/rails/spec/features/sample_spec.rb +7 -0
  84. data/spec/rails/spec/spec_helper.rb +16 -0
  85. data/spec/rails/vendor/assets/javascripts/.keep +0 -0
  86. data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
  87. data/spec/spec_helper.rb +39 -0
  88. data/spec/supports/create_table_file.rb +21 -0
  89. metadata +283 -0
@@ -0,0 +1,173 @@
1
+ require 'set'
2
+ require 'pathname'
3
+ require 'yaml'
4
+
5
+ require_relative 'git'
6
+ require_relative 'utils'
7
+
8
+ module DeltaTest
9
+ class Configuration
10
+
11
+ CONFIG_FILES = [
12
+ 'delta_test.yml',
13
+ 'delta_test.yaml',
14
+ ].freeze
15
+
16
+ attr_accessor *%i[
17
+ base_path
18
+ files
19
+
20
+ table_file
21
+ patterns
22
+ exclude_patterns
23
+ custom_mappings
24
+ ]
25
+
26
+ # for precalculated values
27
+ attr_reader *%i[
28
+ filtered_files
29
+ table_file_path
30
+ ]
31
+
32
+ def initialize
33
+ update do |c|
34
+ c.base_path = File.expand_path('.')
35
+ c.table_file = 'tmp/.delta_test_dt'
36
+ c.files = []
37
+ c.patterns = []
38
+ c.exclude_patterns = []
39
+ c.custom_mappings = {}
40
+ end
41
+ end
42
+
43
+
44
+ # Override setters
45
+ #-----------------------------------------------
46
+ ###
47
+ # Store base_path as Pathname
48
+ #
49
+ # @params {String|Pathname} path
50
+ # @return {Pathname}
51
+ ###
52
+ def base_path=(path)
53
+ @base_path = Pathname.new(path)
54
+ end
55
+
56
+ ###
57
+ # Store table_file as Pathname
58
+ #
59
+ # @params {String|Pathname} path
60
+ # @return {Pathname}
61
+ ###
62
+ def table_file=(path)
63
+ @table_file = Pathname.new(path)
64
+ end
65
+
66
+
67
+ # Update
68
+ #-----------------------------------------------
69
+ ###
70
+ # Update, verify and precalculate
71
+ #
72
+ # @block
73
+ ###
74
+ def update
75
+ yield self if block_given?
76
+ validate!
77
+ precalculate!
78
+ end
79
+
80
+ ###
81
+ # Validate option values
82
+ ###
83
+ def validate!
84
+ if self.base_path.relative?
85
+ raise ValidationError.new(:base_path, 'need to be an absolute path')
86
+ end
87
+
88
+ unless self.files.is_a?(Array)
89
+ raise ValidationError.new(:files, 'need to be an array')
90
+ end
91
+
92
+ unless self.patterns.is_a?(Array)
93
+ raise ValidationError.new(:patterns, 'need to be an array')
94
+ end
95
+
96
+ unless self.exclude_patterns.is_a?(Array)
97
+ raise ValidationError.new(:exclude_patterns, 'need to be an array')
98
+ end
99
+
100
+ unless self.custom_mappings.is_a?(Hash)
101
+ raise ValidationError.new(:custom_mappings, 'need to be a hash')
102
+
103
+ unless self.custom_mappings.values.all? { |v| v.is_a?(Array) }
104
+ raise ValidationError.new(:custom_mappings, 'need to have an array in the contents')
105
+ end
106
+ end
107
+ end
108
+
109
+ ###
110
+ # Precalculate some values
111
+ ###
112
+ def precalculate!
113
+ filtered_files = self.files
114
+ .map { |f| Utils.regulate_filepath(f, self.base_path) }
115
+ .uniq
116
+
117
+ filtered_files = Utils.files_grep(filtered_files, self.patterns, self.exclude_patterns)
118
+
119
+ @filtered_files = Set.new(filtered_files)
120
+
121
+ @table_file_path = Pathname.new(File.absolute_path(self.table_file, self.base_path))
122
+ end
123
+
124
+
125
+ # Auto configuration
126
+ #-----------------------------------------------
127
+ ###
128
+ # Use configuration file and git
129
+ ###
130
+ def auto_configure!
131
+ load_from_file!
132
+ retrive_files_from_git_index!
133
+ update
134
+ end
135
+
136
+ ###
137
+ # Load configuration file
138
+ # And update `base_path` to the directory
139
+ ###
140
+ def load_from_file!
141
+ config_file = Utils.find_file_upward(*CONFIG_FILES)
142
+
143
+ unless config_file
144
+ raise NoConfigurationFileFoundError
145
+ end
146
+
147
+ yaml = YAML.load_file(config_file)
148
+
149
+ self.base_path = File.dirname(config_file)
150
+
151
+ yaml.each do |k, v|
152
+ if self.respond_to?("#{k}=")
153
+ self.send("#{k}=", v)
154
+ else
155
+ raise InvalidOptionError.new(k)
156
+ end
157
+ end
158
+ end
159
+
160
+ ###
161
+ # Retrive files from git index
162
+ # And update `files`
163
+ ###
164
+ def retrive_files_from_git_index!
165
+ unless Git.git_repo?
166
+ raise NotInGitRepositoryError
167
+ end
168
+
169
+ self.files = Git.ls_files
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,83 @@
1
+ require 'fileutils'
2
+ require 'set'
3
+
4
+ require_relative 'utils'
5
+
6
+ module DeltaTest
7
+ class DependenciesTable < ::Hash
8
+
9
+ DEFAULT_PROC = -> (h, k) { h[k] = ::Set.new }
10
+
11
+ def initialize
12
+ super
13
+
14
+ self.default_proc = DEFAULT_PROC
15
+ end
16
+
17
+ ###
18
+ # Restore a table object from a file
19
+ #
20
+ # @params {String|Pathname} file
21
+ ###
22
+ def self.load(file)
23
+ begin
24
+ data = File.binread(file)
25
+ dt = Marshal.load(data)
26
+ dt.default_proc = DEFAULT_PROC
27
+ dt
28
+ rescue
29
+ self.new
30
+ end
31
+ end
32
+
33
+ ###
34
+ # Add a dependency for a spec file
35
+ #
36
+ # @params {String} spec_file
37
+ # @params {String} source_file
38
+ ###
39
+ def add(spec_file, source_file)
40
+ source_file = Utils.regulate_filepath(source_file, DeltaTest.config.base_path)
41
+ self[spec_file] << source_file if DeltaTest.config.filtered_files.include?(source_file)
42
+ end
43
+
44
+ ###
45
+ # Temporary disable default_proc
46
+ # Because Marshal can't dump Hash with default_proc
47
+ #
48
+ # @block
49
+ ###
50
+ def without_default_proc
51
+ self.default_proc = nil
52
+
53
+ begin
54
+ yield
55
+ ensure
56
+ self.default_proc = DEFAULT_PROC
57
+ end
58
+ end
59
+
60
+ ###
61
+ # Cleanup empty sets from the table
62
+ ###
63
+ def cleanup!
64
+ self.reject! { |k, v| v.empty? }
65
+ end
66
+
67
+ ###
68
+ # Dump the table object to a file
69
+ #
70
+ # @params {String|Pathname} file
71
+ ###
72
+ def dump(file)
73
+ # Marshal can't dump hash with default proc
74
+ without_default_proc do
75
+ cleanup!
76
+ data = Marshal.dump(self)
77
+ FileUtils.mkdir_p(File.dirname(file))
78
+ File.open(file, 'wb') { |f| f.write data }
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,55 @@
1
+ module DeltaTest
2
+
3
+ class TableNotFoundError < IOError
4
+
5
+ def initialize(table_file_path)
6
+ @table_file_path = table_file_path
7
+ end
8
+
9
+ def message
10
+ 'table file not found at: `%s`' % @table_file_path
11
+ end
12
+
13
+ end
14
+
15
+ class NotInGitRepositoryError < StandardError
16
+
17
+ def message
18
+ 'the directory is not managed by git'
19
+ end
20
+
21
+ end
22
+
23
+ class NoConfigurationFileFoundError < IOError
24
+
25
+ def message
26
+ 'no configuration file found'
27
+ end
28
+
29
+ end
30
+
31
+ class InvalidOptionError < StandardError
32
+
33
+ def initialize(option)
34
+ @option = option
35
+ end
36
+
37
+ def message
38
+ 'invalid option: %s' % @option
39
+ end
40
+
41
+ end
42
+
43
+ class ValidationError < StandardError
44
+
45
+ def initialize(name, message)
46
+ @name, @_message = name, message
47
+ end
48
+
49
+ def message
50
+ '`%s` %s' % [@name, @_message]
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,101 @@
1
+ require_relative 'analyzer'
2
+ require_relative 'dependencies_table'
3
+
4
+ require_relative 'utils'
5
+
6
+ module DeltaTest
7
+ class Generator
8
+
9
+ attr_reader *%i[
10
+ current_spec_file
11
+ table
12
+ ]
13
+
14
+ ###
15
+ # Setup analyzer and table
16
+ #
17
+ # @params {Boolean} _auto_teardown
18
+ ###
19
+ def setup!(_auto_teardown = true)
20
+ return unless DeltaTest.active?
21
+
22
+ return if @_setup
23
+ @_setup = true
24
+
25
+ DeltaTest.log('--- setup!')
26
+
27
+ @analyzer = Analyzer.new
28
+ @table = DependenciesTable.load(DeltaTest.config.table_file_path)
29
+
30
+ @current_spec_file = nil
31
+
32
+ hook_on_exit { teardown! } if _auto_teardown
33
+ end
34
+
35
+ ###
36
+ # Start analyzer for the spec file
37
+ #
38
+ # @params {String} spec_file
39
+ ###
40
+ def start!(spec_file)
41
+ return unless DeltaTest.active?
42
+
43
+ DeltaTest.log('--- start!(%s)' % spec_file)
44
+
45
+ @current_spec_file = Utils.regulate_filepath(spec_file, DeltaTest.config.base_path).to_s
46
+ @analyzer.start
47
+ end
48
+
49
+ ###
50
+ # Stop analyzer and update table
51
+ ###
52
+ def stop!
53
+ return unless DeltaTest.active?
54
+
55
+ DeltaTest.log('--- stop!')
56
+
57
+ spec_file = @current_spec_file
58
+ @current_spec_file = nil
59
+
60
+ @analyzer.stop
61
+
62
+ if spec_file
63
+ @analyzer.related_source_files.each do |file|
64
+ @table.add(spec_file, file)
65
+ end
66
+ end
67
+ end
68
+
69
+ ###
70
+ # Save table to the file
71
+ ###
72
+ def teardown!
73
+ return unless @_setup
74
+ return if @_teardown
75
+ @_teardown = true
76
+
77
+ DeltaTest.log('--- teardown!')
78
+
79
+ @analyzer.stop
80
+ @table.dump(DeltaTest.config.table_file_path)
81
+ end
82
+
83
+
84
+ private
85
+
86
+ ###
87
+ # Handle exit event
88
+ ###
89
+ def hook_on_exit(&block)
90
+ at_exit do
91
+ if defined?(ParallelTests)
92
+ break unless ParallelTests.first_process?
93
+ ParallelTests.wait_for_other_processes_to_finish
94
+ end
95
+
96
+ block.call
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,88 @@
1
+ require 'open3'
2
+ require 'shellwords'
3
+
4
+ module DeltaTest
5
+ module Git
6
+ class << self
7
+
8
+ ###
9
+ # Check if in git managed directory
10
+ #
11
+ # @return {Boolean}
12
+ ###
13
+ def git_repo?
14
+ o, e, s = exec(%q{git rev-parse --is-inside-work-tree}) rescue []
15
+ !!s && s.success?
16
+ end
17
+
18
+ ###
19
+ # Get root directory of git
20
+ #
21
+ # @return {String}
22
+ ###
23
+ def root_dir
24
+ o, e, s = exec(%q{git rev-parse --show-toplevel})
25
+ s.success? ? o.strip : nil
26
+ end
27
+
28
+ ###
29
+ # Get commit id from rev name
30
+ #
31
+ # @params {String} rev - e.g., branch name
32
+ #
33
+ # @return {String}
34
+ ###
35
+ def rev_parse(rev)
36
+ o, e, s = exec(%q{git rev-parse %s}, rev)
37
+ s.success? ? o.strip : nil
38
+ end
39
+
40
+ ###
41
+ # Compare two rev names by their commit ids
42
+ #
43
+ # @params {String} r1
44
+ # @params {String} r2
45
+ #
46
+ # @return {Boolean}
47
+ ###
48
+ def same_commit?(r1, r2)
49
+ rev_parse(r1) == rev_parse(r2)
50
+ end
51
+
52
+ ###
53
+ # Get file list from git index
54
+ #
55
+ # @return {Array<String>}
56
+ ###
57
+ def ls_files
58
+ o, e, s = exec(%q{git ls-files -z})
59
+ s.success? ? o.split("\x0") : []
60
+ end
61
+
62
+ ###
63
+ # Get list of modified files in diff
64
+ #
65
+ # @params {String} base
66
+ # @params {String} head
67
+ #
68
+ # @return {Array<String>}
69
+ ###
70
+ def changed_files(base = 'master', head = 'HEAD')
71
+ o, e, s = exec(%q{git --no-pager diff --name-only -z %s %s}, base, head)
72
+ s.success? ? o.split("\x0") : []
73
+ end
74
+
75
+
76
+ private
77
+
78
+ ###
79
+ # Util for executing command
80
+ ###
81
+ def exec(command, *args)
82
+ args = args.map { |a| Shellwords.escape(a) }
83
+ Open3.capture3(command % args, chdir: DeltaTest.config.base_path)
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,64 @@
1
+ require 'set'
2
+
3
+ require_relative 'git'
4
+ require_relative 'dependencies_table'
5
+
6
+ module DeltaTest
7
+ class RelatedSpecList
8
+
9
+ attr_reader *%i[
10
+ table
11
+ changed_files
12
+ ]
13
+
14
+ ###
15
+ # Load table from the file
16
+ ###
17
+ def load_table!
18
+ unless File.exist?(DeltaTest.config.table_file_path)
19
+ raise TableNotFoundError.new(DeltaTest.config.table_file_path)
20
+ end
21
+
22
+ @table = DependenciesTable.load(DeltaTest.config.table_file_path)
23
+ end
24
+
25
+ ###
26
+ # Retrive changed files in git diff
27
+ #
28
+ # @params {String} base
29
+ # @params {String} head
30
+ ###
31
+ def retrive_changed_files!(base, head)
32
+ unless Git.git_repo?
33
+ raise NotInGitRepositoryError
34
+ end
35
+
36
+ @changed_files = Git.changed_files(base, head)
37
+ end
38
+
39
+ ###
40
+ # Calculate related spec files
41
+ #
42
+ # @return {Set<String>}
43
+ ###
44
+ def related_spec_files
45
+ spec_files = Set.new
46
+
47
+ @table.each do |spec_file, dependencies|
48
+ related = @changed_files.include?(spec_file) \
49
+ || (dependencies & @changed_files).any?
50
+
51
+ spec_files << spec_file if related
52
+ end
53
+
54
+ DeltaTest.config.custom_mappings.each do |spec_file, patterns|
55
+ if Utils.files_grep(@changed_files, patterns).any?
56
+ spec_files << spec_file
57
+ end
58
+ end
59
+
60
+ spec_files
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'generator'
2
+
3
+ module DeltaTest
4
+ module SpecHelpers
5
+
6
+ ###
7
+ # Setup generator and hook analyzer on contexts
8
+ ###
9
+ def use_delta_test(example)
10
+ $delta_test_generator ||= DeltaTest::Generator.new
11
+ $delta_test_generator.setup!
12
+
13
+ example.before(:context) do
14
+ $delta_test_generator.start!(example.metadata[:file_path])
15
+ end
16
+
17
+ example.after(:context) do
18
+ $delta_test_generator.stop!
19
+ end
20
+ end
21
+
22
+ ###
23
+ # Extend
24
+ #
25
+ # @params {} example
26
+ ###
27
+ def self.extended(example)
28
+ example.use_delta_test(example)
29
+ end
30
+
31
+ ###
32
+ # Include
33
+ # calls `extend` internally
34
+ #
35
+ # @params {} example
36
+ ###
37
+ def self.included(example)
38
+ example.extend(self)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,93 @@
1
+ module DeltaTest
2
+ module Utils
3
+ class << self
4
+
5
+ ###
6
+ # Convert to relative and clean path
7
+ #
8
+ # @params {String|Pathname} file
9
+ # @params {Pathname} base_path
10
+ #
11
+ # @return {Pathname}
12
+ ###
13
+ def regulate_filepath(file, base_path)
14
+ file = Pathname.new(file)
15
+ file = file.relative_path_from(base_path) rescue file
16
+ file.cleanpath
17
+ end
18
+
19
+ ###
20
+ # Find file upward from pwd
21
+ #
22
+ # @params {String} file_names
23
+ #
24
+ # @return {String}
25
+ ###
26
+ def find_file_upward(*file_names)
27
+ pwd = Dir.pwd
28
+ base = Hash.new { |h, k| h[k] = pwd }
29
+ file = {}
30
+
31
+ while base.values.all? { |b| '.' != b && '/' != b }
32
+ file_names.each do |name|
33
+ file[name] = File.join(base[name], name)
34
+ base[name] = File.dirname(base[name])
35
+
36
+ return file[name] if File.exists?(file[name])
37
+ end
38
+ end
39
+
40
+ nil
41
+ end
42
+
43
+ ###
44
+ # Wildcard pattern matching against a file list
45
+ #
46
+ # @params {Array<T as String|Pathname>} files
47
+ # @params {Array<String>} patterns
48
+ # @params {Array<String>} exclude_patterns
49
+ #
50
+ # @return {Array<T>}
51
+ ###
52
+ def files_grep(files, patterns = [], exclude_patterns = [])
53
+ patterns = patterns
54
+ .map { |p| grep_pattern_to_regexp(p) }
55
+ exclude_patterns = exclude_patterns
56
+ .map { |p| grep_pattern_to_regexp(p) }
57
+
58
+ any_patterns = patterns.any?
59
+ any_exclude_patterns = exclude_patterns.any?
60
+
61
+ files.select do |file|
62
+ matcher = ->(p) { p === file.to_s }
63
+
64
+ (
65
+ !any_patterns || patterns.any?(&matcher)
66
+ ) && (
67
+ !any_exclude_patterns || !exclude_patterns.any?(&matcher)
68
+ )
69
+ end
70
+ end
71
+
72
+
73
+ private
74
+
75
+ ###
76
+ # Convert file wildcard pattern to a regular expression
77
+ #
78
+ # @params {String} pattern
79
+ #
80
+ # @return {String}
81
+ ###
82
+ def grep_pattern_to_regexp(pattern)
83
+ pattern = Regexp.escape(pattern)
84
+ .gsub('\*\*/', '.*/?')
85
+ .gsub('\*\*', '.*')
86
+ .gsub('\*', '[^/]*')
87
+
88
+ Regexp.new('^%s$' % pattern)
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,9 @@
1
+ module DeltaTest
2
+
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ REVISION = 0
6
+
7
+ VERSION = [MAJOR, MINOR, REVISION].compact.join('.')
8
+
9
+ end