cranium 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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +3 -0
- data/Vagrantfile +24 -0
- data/bin/cranium +9 -0
- data/config/cucumber.yml +9 -0
- data/cranium.gemspec +26 -0
- data/db/setup.sql +8 -0
- data/docker-compose.yml +8 -0
- data/examples/config.rb +14 -0
- data/examples/deduplication.rb +27 -0
- data/examples/import_csv_with_field_lookup_inserting_new_dimension_keys.rb +26 -0
- data/examples/incremental_extract.rb +17 -0
- data/examples/lookup_with_multiple_fields.rb +25 -0
- data/features/archive.feature +49 -0
- data/features/extract/incremental_extract.feature +56 -0
- data/features/extract/simple_extract.feature +85 -0
- data/features/import/import_csv_to_database_as_delta.feature +38 -0
- data/features/import/import_csv_to_database_with_delete_insert_merging.feature +51 -0
- data/features/import/import_csv_to_database_with_truncate_insert.feature +49 -0
- data/features/import/import_csv_to_database_with_update_merging.feature +46 -0
- data/features/import/import_csv_with_always_inserting_new_dimension_keys.feature +137 -0
- data/features/import/import_csv_with_field_lookup_inserting_new_dimension_keys.feature +62 -0
- data/features/import/import_csv_with_field_lookup_transformation.feature +125 -0
- data/features/import/import_csv_with_transformation.feature +55 -0
- data/features/import/import_multiple_csv_files_without_transformations.feature +44 -0
- data/features/import/import_with_load_id_from_sequence.feature +53 -0
- data/features/import/import_with_lookup_from_multiple_fields.feature +64 -0
- data/features/read.feature +56 -0
- data/features/remove.feature +44 -0
- data/features/restore_database_connection.feature +55 -0
- data/features/step_definitions/database_table_steps.rb +40 -0
- data/features/step_definitions/definition_steps.rb +3 -0
- data/features/step_definitions/execution_steps.rb +23 -0
- data/features/step_definitions/file_steps.rb +39 -0
- data/features/support/class_extensions.rb +24 -0
- data/features/support/env.rb +27 -0
- data/features/support/randomize.rb +22 -0
- data/features/support/stop_on_first_error.rb +5 -0
- data/features/transform/deduplication.feature +37 -0
- data/features/transform/empty_transformation.feature +72 -0
- data/features/transform/join.feature +180 -0
- data/features/transform/join_multiple_files_into_one_output_file.feature +46 -0
- data/features/transform/output_rows.feature +70 -0
- data/features/transform/projection.feature +34 -0
- data/features/transform/raw_ruby_transformation.feature +69 -0
- data/features/transform/split_field.feature +39 -0
- data/lib/cranium/application.rb +104 -0
- data/lib/cranium/archiver.rb +36 -0
- data/lib/cranium/attribute_dsl.rb +43 -0
- data/lib/cranium/command_line_options.rb +27 -0
- data/lib/cranium/configuration.rb +33 -0
- data/lib/cranium/data_importer.rb +35 -0
- data/lib/cranium/data_reader.rb +48 -0
- data/lib/cranium/data_transformer.rb +126 -0
- data/lib/cranium/database.rb +36 -0
- data/lib/cranium/definition_registry.rb +21 -0
- data/lib/cranium/dimension_manager.rb +65 -0
- data/lib/cranium/dsl/database_definition.rb +23 -0
- data/lib/cranium/dsl/extract_definition.rb +28 -0
- data/lib/cranium/dsl/import_definition.rb +50 -0
- data/lib/cranium/dsl/source_definition.rb +67 -0
- data/lib/cranium/dsl.rb +100 -0
- data/lib/cranium/extensions/file.rb +7 -0
- data/lib/cranium/extensions/sequel_greenplum.rb +30 -0
- data/lib/cranium/external_table.rb +75 -0
- data/lib/cranium/extract/data_extractor.rb +11 -0
- data/lib/cranium/extract/storage.rb +57 -0
- data/lib/cranium/extract/strategy/base.rb +27 -0
- data/lib/cranium/extract/strategy/incremental.rb +16 -0
- data/lib/cranium/extract/strategy/simple.rb +9 -0
- data/lib/cranium/extract/strategy.rb +7 -0
- data/lib/cranium/extract.rb +7 -0
- data/lib/cranium/import_strategy/base.rb +55 -0
- data/lib/cranium/import_strategy/delete_insert.rb +40 -0
- data/lib/cranium/import_strategy/delta.rb +8 -0
- data/lib/cranium/import_strategy/merge.rb +50 -0
- data/lib/cranium/import_strategy/truncate_insert.rb +19 -0
- data/lib/cranium/import_strategy.rb +9 -0
- data/lib/cranium/logging.rb +15 -0
- data/lib/cranium/profiling.rb +13 -0
- data/lib/cranium/progress_output.rb +37 -0
- data/lib/cranium/sequel/hash.rb +32 -0
- data/lib/cranium/sequel.rb +5 -0
- data/lib/cranium/source_registry.rb +21 -0
- data/lib/cranium/test_framework/cucumber_table.rb +140 -0
- data/lib/cranium/test_framework/database_entity.rb +29 -0
- data/lib/cranium/test_framework/database_sequence.rb +16 -0
- data/lib/cranium/test_framework/database_table.rb +33 -0
- data/lib/cranium/test_framework/upload_directory.rb +39 -0
- data/lib/cranium/test_framework/world.rb +66 -0
- data/lib/cranium/test_framework.rb +10 -0
- data/lib/cranium/transformation/duplication_index.rb +42 -0
- data/lib/cranium/transformation/index.rb +83 -0
- data/lib/cranium/transformation/join.rb +141 -0
- data/lib/cranium/transformation/sequence.rb +42 -0
- data/lib/cranium/transformation.rb +8 -0
- data/lib/cranium/transformation_record.rb +45 -0
- data/lib/cranium.rb +57 -0
- data/rake/test.rake +31 -0
- data/spec/cranium/application_spec.rb +166 -0
- data/spec/cranium/archiver_spec.rb +44 -0
- data/spec/cranium/command_line_options_spec.rb +32 -0
- data/spec/cranium/configuration_spec.rb +31 -0
- data/spec/cranium/data_importer_spec.rb +55 -0
- data/spec/cranium/data_transformer_spec.rb +16 -0
- data/spec/cranium/database_spec.rb +69 -0
- data/spec/cranium/definition_registry_spec.rb +45 -0
- data/spec/cranium/dimension_manager_spec.rb +63 -0
- data/spec/cranium/dsl/database_definition_spec.rb +23 -0
- data/spec/cranium/dsl/extract_definition_spec.rb +76 -0
- data/spec/cranium/dsl/import_definition_spec.rb +153 -0
- data/spec/cranium/dsl/source_definition_spec.rb +84 -0
- data/spec/cranium/dsl_spec.rb +119 -0
- data/spec/cranium/external_table_spec.rb +71 -0
- data/spec/cranium/extract/storage_spec.rb +125 -0
- data/spec/cranium/logging_spec.rb +37 -0
- data/spec/cranium/sequel/hash_spec.rb +56 -0
- data/spec/cranium/source_registry_spec.rb +31 -0
- data/spec/cranium/test_framework/cucumber_table_spec.rb +144 -0
- data/spec/cranium/transformation/duplication_index_spec.rb +75 -0
- data/spec/cranium/transformation/index_spec.rb +178 -0
- data/spec/cranium/transformation/join_spec.rb +43 -0
- data/spec/cranium/transformation/sequence_spec.rb +83 -0
- data/spec/cranium/transformation_record_spec.rb +78 -0
- data/spec/cranium_spec.rb +53 -0
- data/spec/spec_helper.rb +1 -0
- metadata +362 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
class Cranium::Transformation::Join
|
|
4
|
+
|
|
5
|
+
attr_accessor :source_left, :source_right, :target, :match_fields, :type
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def execute
|
|
10
|
+
validate_parameters
|
|
11
|
+
cache_commonly_used_values
|
|
12
|
+
join_sources
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def validate_parameters
|
|
20
|
+
raise "Missing left source for join transformation" if source_left.nil?
|
|
21
|
+
raise "Missing right source for join transformation" if source_right.nil?
|
|
22
|
+
raise "Missing target for join transformation" if target.nil?
|
|
23
|
+
raise "Invalid match fields for join transformation" unless match_fields.nil? or match_fields.is_a? Hash
|
|
24
|
+
raise "Invalid type for join transformation" unless %i(inner left).include?(type)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cache_commonly_used_values
|
|
30
|
+
@left_source_field_names = source_left.fields.keys
|
|
31
|
+
@right_source_field_names = source_right.fields.keys
|
|
32
|
+
@target_field_names = target.fields.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def join_sources
|
|
38
|
+
build_join_table_from_right_source_files
|
|
39
|
+
write_output_file
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_join_table_from_right_source_files
|
|
45
|
+
@join_table = {}
|
|
46
|
+
source_right.files.each do |file|
|
|
47
|
+
build_join_table_from_file file
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_join_table_from_file(file)
|
|
54
|
+
line_number = 0
|
|
55
|
+
CSV.foreach File.join(Cranium.configuration.upload_path, file), csv_read_options_for(source_right) do |row|
|
|
56
|
+
next if 1 == (line_number += 1)
|
|
57
|
+
|
|
58
|
+
record = Hash[@right_source_field_names.zip row]
|
|
59
|
+
index_key = right_index_key_for record
|
|
60
|
+
if @join_table.has_key? index_key
|
|
61
|
+
@join_table[index_key] << record
|
|
62
|
+
else
|
|
63
|
+
@join_table[index_key] = [record]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_output_file
|
|
71
|
+
CSV.open "#{Cranium.configuration.upload_path}/#{target.file}", "w:#{target.encoding}", csv_write_options_for(target) do |target_file|
|
|
72
|
+
@target_file = target_file
|
|
73
|
+
source_left.files.each do |file|
|
|
74
|
+
process_left_source_file File.join(Cranium.configuration.upload_path, file), target_file
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@target.resolve_files
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def process_left_source_file(input_file, output_file)
|
|
84
|
+
line_number = 0
|
|
85
|
+
CSV.foreach input_file, csv_read_options_for(source_left) do |row|
|
|
86
|
+
next if 1 == (line_number += 1)
|
|
87
|
+
|
|
88
|
+
record = Hash[@left_source_field_names.zip row]
|
|
89
|
+
|
|
90
|
+
joined_records = joined_records_for(record)
|
|
91
|
+
joined_records << record if joined_records.empty? && type == :left
|
|
92
|
+
|
|
93
|
+
joined_records.each do |record_to_output|
|
|
94
|
+
output_file << @target_field_names.map { |field| record_to_output[field] }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def joined_records_for(record)
|
|
102
|
+
index_key = left_index_key_for record
|
|
103
|
+
return [] unless @join_table.has_key? index_key
|
|
104
|
+
@join_table[index_key].map { |matching_record| record.merge matching_record }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def left_index_key_for(record)
|
|
110
|
+
record.select { |field, _| match_fields.values.include? field }.values
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def right_index_key_for(record)
|
|
116
|
+
record.select { |field, _| match_fields.keys.include? field }.values
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def csv_write_options_for(source_definition)
|
|
122
|
+
{
|
|
123
|
+
col_sep: source_definition.delimiter,
|
|
124
|
+
quote_char: source_definition.quote,
|
|
125
|
+
write_headers: true,
|
|
126
|
+
headers: source_definition.fields.keys
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def csv_read_options_for(source_definition)
|
|
133
|
+
{
|
|
134
|
+
encoding: source_definition.encoding,
|
|
135
|
+
col_sep: source_definition.delimiter,
|
|
136
|
+
quote_char: source_definition.quote,
|
|
137
|
+
return_headers: false
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class Cranium::Transformation::Sequence
|
|
2
|
+
|
|
3
|
+
attr_reader :name
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def initialize(name)
|
|
8
|
+
@name = name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def next_value
|
|
14
|
+
if @current_value.nil?
|
|
15
|
+
@current_value = Cranium::Database.connection["SELECT nextval('#{@name}') AS next_value"].first[:next_value]
|
|
16
|
+
else
|
|
17
|
+
@current_value += 1
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def flush
|
|
24
|
+
Cranium::Database.connection.run "SELECT setval('#{@name}', #{@current_value})" unless @current_value.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
|
|
31
|
+
def by_name(name)
|
|
32
|
+
@sequences ||= {}
|
|
33
|
+
if @sequences[name].nil?
|
|
34
|
+
@sequences[name] = new name
|
|
35
|
+
Cranium.application.after_import { @sequences[name].flush }
|
|
36
|
+
end
|
|
37
|
+
@sequences[name]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module Cranium::Transformation
|
|
2
|
+
|
|
3
|
+
autoload :DuplicationIndex, 'cranium/transformation/duplication_index'
|
|
4
|
+
autoload :Index, 'cranium/transformation/index'
|
|
5
|
+
autoload :Join, 'cranium/transformation/join'
|
|
6
|
+
autoload :Sequence, 'cranium/transformation/sequence'
|
|
7
|
+
|
|
8
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Cranium::TransformationRecord
|
|
2
|
+
|
|
3
|
+
attr_reader :data
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def initialize(source_fields, target_fields)
|
|
8
|
+
@source_fields, @target_fields = source_fields, target_fields
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def input_data=(values)
|
|
14
|
+
@data = Hash[@source_fields.zip values]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def [](field)
|
|
20
|
+
@data[field]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def []=(field, value)
|
|
26
|
+
@data[field] = value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def split_field(field, options)
|
|
32
|
+
values = @data[field].split(options[:by])
|
|
33
|
+
|
|
34
|
+
options[:into].each_with_index do |target_field, index|
|
|
35
|
+
@data[target_field] = values[index] || options[:default_value] || values.last
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def has_key?(key)
|
|
42
|
+
@data.has_key? key
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
data/lib/cranium.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Cranium
|
|
2
|
+
|
|
3
|
+
autoload :Application, 'cranium/application'
|
|
4
|
+
autoload :Archiver, 'cranium/archiver'
|
|
5
|
+
autoload :AttributeDSL, 'cranium/attribute_dsl'
|
|
6
|
+
autoload :CommandLineOptions, 'cranium/command_line_options'
|
|
7
|
+
autoload :Configuration, 'cranium/configuration'
|
|
8
|
+
autoload :Database, 'cranium/database'
|
|
9
|
+
autoload :DataImporter, 'cranium/data_importer'
|
|
10
|
+
autoload :DataReader, 'cranium/data_reader'
|
|
11
|
+
autoload :DataTransformer, 'cranium/data_transformer'
|
|
12
|
+
autoload :DefinitionRegistry, 'cranium/definition_registry'
|
|
13
|
+
autoload :DimensionManager, 'cranium/dimension_manager'
|
|
14
|
+
autoload :DSL, 'cranium/dsl'
|
|
15
|
+
autoload :ExternalTable, 'cranium/external_table'
|
|
16
|
+
autoload :Extract, 'cranium/extract'
|
|
17
|
+
autoload :ImportStrategy, 'cranium/import_strategy'
|
|
18
|
+
autoload :Logging, 'cranium/logging'
|
|
19
|
+
autoload :ProgressOutput, 'cranium/progress_output'
|
|
20
|
+
autoload :Sequel, 'cranium/sequel'
|
|
21
|
+
autoload :SourceRegistry, 'cranium/source_registry'
|
|
22
|
+
autoload :TestFramework, 'cranium/test_framework'
|
|
23
|
+
autoload :TransformationRecord, 'cranium/transformation_record'
|
|
24
|
+
autoload :Transformation, 'cranium/transformation'
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
|
|
28
|
+
def application(argv = [])
|
|
29
|
+
@application ||= Application.new(argv)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def configuration
|
|
35
|
+
@configuration ||= Configuration.new.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def configure
|
|
41
|
+
mutable_configuration = configuration.dup
|
|
42
|
+
yield mutable_configuration
|
|
43
|
+
@configuration = mutable_configuration
|
|
44
|
+
@configuration.freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_arguments
|
|
50
|
+
application.load_arguments
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
self.extend Cranium::DSL
|
data/rake/test.rake
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'cucumber'
|
|
2
|
+
require 'cucumber/rake/task'
|
|
3
|
+
require 'rspec/core/rake_task'
|
|
4
|
+
|
|
5
|
+
task :default => :test
|
|
6
|
+
|
|
7
|
+
desc "Run test suite (all RSpec examples and Cucumber features)"
|
|
8
|
+
task :test => [:'test:spec', :'test:features']
|
|
9
|
+
|
|
10
|
+
desc "Run RSpec code examples (options: RSPEC_SEED=seed)"
|
|
11
|
+
task :spec => :'test:spec'
|
|
12
|
+
|
|
13
|
+
desc "Run Cucumber features (options: CUCUMBER_SEED=seed)"
|
|
14
|
+
task :features => :'test:features'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
namespace :test do
|
|
18
|
+
|
|
19
|
+
desc "Run RSpec code examples (options: RSPEC_SEED=seed)"
|
|
20
|
+
RSpec::Core::RakeTask.new :spec do |task|
|
|
21
|
+
task.verbose = false
|
|
22
|
+
task.rspec_opts = "--order random"
|
|
23
|
+
task.rspec_opts << " --seed #{ENV['RSPEC_SEED']}" if ENV['RSPEC_SEED']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Cucumber::Rake::Task.new(:features, "Run Cucumber features (options: CUCUMBER_SEED=seed)") do |task|
|
|
28
|
+
task.cucumber_opts = %w[--profile build]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Cranium::Application do
|
|
4
|
+
|
|
5
|
+
let(:application) { Cranium::Application.new [] }
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe "#load_arguments" do
|
|
9
|
+
it "should return the provided load arguments" do
|
|
10
|
+
app = Cranium::Application.new ["--cranium-load", "load", "--customer_name", "my_customer"]
|
|
11
|
+
expect(app.load_arguments).to eq customer_name: "my_customer"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "#cranium_arguments" do
|
|
16
|
+
it "should return the provided load arguments" do
|
|
17
|
+
app = Cranium::Application.new ["--cranium-load", "loads/load_file", "--customer_name", "my_customer"]
|
|
18
|
+
expect(app.cranium_arguments).to eq load: "loads/load_file"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
describe "Application" do
|
|
24
|
+
it "should include metrics logging capabilities" do
|
|
25
|
+
expect(application.respond_to?(:log)).to be_truthy
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
describe "#sources" do
|
|
31
|
+
it "should return a SourceRegistry" do
|
|
32
|
+
expect(application.sources).to be_a Cranium::SourceRegistry
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
describe "#register_source" do
|
|
38
|
+
it "should register a source in the source registry and resolve its files" do
|
|
39
|
+
source = double "SourceDefinition"
|
|
40
|
+
|
|
41
|
+
expect(application.sources).to receive(:register_source).with(:source1).and_return(source)
|
|
42
|
+
expect(source).to receive(:resolve_files)
|
|
43
|
+
|
|
44
|
+
application.register_source(:source1) { file "test*.csv" }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
describe "#run" do
|
|
50
|
+
before(:each) do
|
|
51
|
+
@original_stderr = $stderr
|
|
52
|
+
$stderr = StringIO.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
after(:each) do
|
|
56
|
+
$stderr = @original_stderr
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context "when no files are specified as an argument" do
|
|
60
|
+
it "should exit with an error" do
|
|
61
|
+
expect { application.run }.to raise_error(SystemExit) { |exit| expect(exit.status).to eq(1) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "should log an error to STDOUT" do
|
|
65
|
+
expect { application.run }.to raise_error(SystemExit)
|
|
66
|
+
|
|
67
|
+
expect($stderr.string.chomp).to eq "ERROR: No file specified"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
context "when a non-existent file is specified as an argument" do
|
|
73
|
+
let(:application) { Cranium::Application.new ["--cranium-load", "no-such-file.exists"] }
|
|
74
|
+
|
|
75
|
+
it "should exit with an error" do
|
|
76
|
+
expect { application.run }.to raise_error(SystemExit) { |exit| expect(exit.status).to eq(1) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "should log an error to STDOUT" do
|
|
80
|
+
expect { application.run }.to raise_error(SystemExit)
|
|
81
|
+
|
|
82
|
+
expect($stderr.string.chomp).to eq "ERROR: File 'no-such-file.exists' does not exist"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
context "when called with an existing file" do
|
|
88
|
+
let(:file) { "products.rb" }
|
|
89
|
+
|
|
90
|
+
let(:application) { Cranium::Application.new ["--cranium-load", file] }
|
|
91
|
+
|
|
92
|
+
before(:each) do
|
|
93
|
+
allow(File).to receive(:exists?).with(file).and_return(true)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
it "should load the first file specified as a command line parameter" do
|
|
98
|
+
expect(application).to receive(:load).with(file)
|
|
99
|
+
|
|
100
|
+
application.run
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
it "should run any registered after hooks" do
|
|
105
|
+
allow(application).to receive :load
|
|
106
|
+
|
|
107
|
+
hook_ran = false
|
|
108
|
+
application.register_hook :after do
|
|
109
|
+
hook_ran = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
application.run
|
|
113
|
+
|
|
114
|
+
expect(hook_ran).to be_truthy
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
context "when the execution of the process raises an error" do
|
|
119
|
+
let(:error) { StandardError.new }
|
|
120
|
+
before(:each) { allow(application).to receive(:load).and_raise error }
|
|
121
|
+
|
|
122
|
+
it "should propagate the error" do
|
|
123
|
+
expect { application.run }.to raise_error
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "should log an error" do
|
|
127
|
+
expect(application).to receive(:log).with(:error, error)
|
|
128
|
+
|
|
129
|
+
expect { application.run }.to raise_error
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "should still run any registered after hooks" do
|
|
133
|
+
hook_ran = false
|
|
134
|
+
application.register_hook :after do
|
|
135
|
+
hook_ran = true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
application.run
|
|
140
|
+
rescue
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
expect(hook_ran).to be_truthy
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
describe "#after_import" do
|
|
151
|
+
|
|
152
|
+
it "should register the given block" do
|
|
153
|
+
block_called = false
|
|
154
|
+
|
|
155
|
+
application.after_import do
|
|
156
|
+
block_called = true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
application.apply_hook(:after_import)
|
|
160
|
+
|
|
161
|
+
expect(block_called).to be_truthy
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Cranium::Archiver do
|
|
4
|
+
|
|
5
|
+
before(:each) do
|
|
6
|
+
allow(Cranium).to receive_messages(configuration: Cranium::Configuration.new.tap do |config|
|
|
7
|
+
config.gpfdist_home_directory = "gpfdist_home"
|
|
8
|
+
config.upload_directory = "upload_dir"
|
|
9
|
+
config.archive_directory = "path/to/archive"
|
|
10
|
+
end)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
describe ".archive" do
|
|
15
|
+
it "should create the archive directory if it doesn't exist" do
|
|
16
|
+
allow(Dir).to receive(:exists?).with("path/to/archive").and_return(false)
|
|
17
|
+
|
|
18
|
+
expect(FileUtils).to receive(:mkpath).with "path/to/archive"
|
|
19
|
+
|
|
20
|
+
Cranium::Archiver.archive
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should move files to the archive directory" do
|
|
24
|
+
allow(Dir).to receive(:exists?).with("path/to/archive").and_return(true)
|
|
25
|
+
allow(Time).to receive(:now).and_return Time.new(2000, 1, 1, 1, 2, 3)
|
|
26
|
+
|
|
27
|
+
expect(FileUtils).to receive(:mv).with "gpfdist_home/upload_dir/file.txt", "path/to/archive/2000-01-01_01h02m03s_file.txt"
|
|
28
|
+
expect(FileUtils).to receive(:mv).with "gpfdist_home/upload_dir/another_file.txt", "path/to/archive/2000-01-01_01h02m03s_another_file.txt"
|
|
29
|
+
|
|
30
|
+
Cranium::Archiver.archive "file.txt", "another_file.txt"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
describe ".remove" do
|
|
36
|
+
it "should remove files from the upload directory" do
|
|
37
|
+
expect(FileUtils).to receive(:rm).with "gpfdist_home/upload_dir/file.txt"
|
|
38
|
+
expect(FileUtils).to receive(:rm).with "gpfdist_home/upload_dir/another_file.txt"
|
|
39
|
+
|
|
40
|
+
Cranium::Archiver.remove "file.txt", "another_file.txt"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Cranium::CommandLineOptions do
|
|
4
|
+
|
|
5
|
+
let(:argv) {
|
|
6
|
+
%w[
|
|
7
|
+
--cranium-initializer my_initializer
|
|
8
|
+
--cranium-load my_load
|
|
9
|
+
--some-param some_value
|
|
10
|
+
--another-param another_value
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
subject { Cranium::CommandLineOptions.new argv }
|
|
16
|
+
|
|
17
|
+
describe "#cranium_arguments" do
|
|
18
|
+
|
|
19
|
+
it "should return only arguments used by Cranium" do
|
|
20
|
+
expect(subject.cranium_arguments).to eq(initializer: "my_initializer", load: "my_load")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
describe "#load_arguments" do
|
|
27
|
+
it "should return non-cranium arguments" do
|
|
28
|
+
expect(subject.load_arguments).to eq(:"some-param" => "some_value", :"another-param" => "another_value")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Cranium::Configuration do
|
|
4
|
+
|
|
5
|
+
let(:config) { Cranium::Configuration.new }
|
|
6
|
+
|
|
7
|
+
describe "#upload_path" do
|
|
8
|
+
it "should return the full upload path" do
|
|
9
|
+
config.gpfdist_home_directory = "/gpfdist/home/dir"
|
|
10
|
+
config.upload_directory = "uploads/customer"
|
|
11
|
+
|
|
12
|
+
expect(config.upload_path).to eq "/gpfdist/home/dir/uploads/customer"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
describe "#storage_directory" do
|
|
18
|
+
it "should return the previously set value" do
|
|
19
|
+
config.storage_directory = "/some/path"
|
|
20
|
+
expect(config.storage_directory).to eq "/some/path"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should return the default storage directory if one wasn't explicitly set" do
|
|
24
|
+
config.gpfdist_home_directory = "/gpfdist/home/dir"
|
|
25
|
+
config.upload_directory = "uploads/customer"
|
|
26
|
+
|
|
27
|
+
expect(config.storage_directory).to eq "/gpfdist/home/dir/uploads/customer/.cranium"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Cranium::DataImporter do
|
|
4
|
+
|
|
5
|
+
before do
|
|
6
|
+
connection = double
|
|
7
|
+
allow(Cranium::Database).to receive(:connection).and_return connection
|
|
8
|
+
allow(connection).to receive(:transaction).and_yield
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:importer) { Cranium::DataImporter.new }
|
|
12
|
+
let(:definition) { Cranium::DSL::ImportDefinition.new "definition_name" }
|
|
13
|
+
|
|
14
|
+
describe "#import" do
|
|
15
|
+
|
|
16
|
+
context "when called with both merge and delete_insert fields set" do
|
|
17
|
+
it "should raise an exception" do
|
|
18
|
+
definition.delete_insert_on :some_field
|
|
19
|
+
definition.merge_on :another_field
|
|
20
|
+
|
|
21
|
+
expect { importer.import(definition) }.to raise_error StandardError, "Import should not combine merge_on, delete_insert_on and truncate_insert settings"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context "when called with both merge and truncate_insert fields set" do
|
|
26
|
+
it "should raise an exception" do
|
|
27
|
+
definition.truncate_insert true
|
|
28
|
+
definition.merge_on :another_field
|
|
29
|
+
|
|
30
|
+
expect { importer.import(definition) }.to raise_error StandardError, "Import should not combine merge_on, delete_insert_on and truncate_insert settings"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context "when called with both delete_insert and truncate_insert fields set" do
|
|
35
|
+
it "should raise an exception" do
|
|
36
|
+
definition.delete_insert_on :some_field
|
|
37
|
+
definition.truncate_insert true
|
|
38
|
+
|
|
39
|
+
expect { importer.import(definition) }.to raise_error StandardError, "Import should not combine merge_on, delete_insert_on and truncate_insert settings"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "when called with both merge, delete_insert and truncate_insert fields set" do
|
|
44
|
+
it "should raise an exception" do
|
|
45
|
+
definition.delete_insert_on :some_field
|
|
46
|
+
definition.merge_on :another_field
|
|
47
|
+
definition.truncate_insert true
|
|
48
|
+
|
|
49
|
+
expect { importer.import(definition) }.to raise_error StandardError, "Import should not combine merge_on, delete_insert_on and truncate_insert settings"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Cranium::DataTransformer do
|
|
4
|
+
|
|
5
|
+
describe "#transform" do
|
|
6
|
+
it "should raise an error if the target definition's file name has been overriden" do
|
|
7
|
+
source = Cranium::DSL::SourceDefinition.new :source
|
|
8
|
+
target = Cranium::DSL::SourceDefinition.new :target
|
|
9
|
+
|
|
10
|
+
target.file "overriden filename"
|
|
11
|
+
|
|
12
|
+
expect { Cranium::DataTransformer.new(source, target).transform }.to raise_error StandardError, "Source definition 'target' cannot overrride the file name because it is a transformation target"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|