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