Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions spec/linters/migration/no_model_in_specs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ class NoModelInSpecs < RuboCop::Cop::Base
'Use raw Sequel operations (e.g. db[:table].insert) instead. ' \
'See spec/migrations/Readme.md for details.'.freeze

def on_new_investigation
@model_let_names = Set.new
return unless processed_source.ast

processed_source.ast.each_descendant(:block) do |node|
next unless %i[let let!].include?(node.send_node&.method_name)

let_name_node = node.send_node.first_argument
next unless let_name_node&.sym_type?

body = node.body
next unless body

# let(:foo) { SomeModel } or let(:foo) { VCAP::CloudController::SomeModel }
inner = body.begin_type? ? body.children.last : body
@model_let_names << let_name_node.value if model_const_node?(inner)
end
end

def on_send(node)
add_offense(node) if model_receiver?(node.receiver)
end
Expand All @@ -14,9 +33,20 @@ def on_send(node)

def model_receiver?(receiver)
return false unless receiver
return false unless receiver.const_type?

name = receiver.const_name.to_s
return model_const_node?(receiver) if receiver.const_type?
return @model_let_names.include?(receiver.method_name) if receiver.send_type?

false
end

def model_const_node?(node)
return false unless node&.const_type?

model_class_name?(node.const_name.to_s)
end

def model_class_name?(name)
name.end_with?('Model') && name != 'Sequel::Model'
end
end
Expand Down
28 changes: 27 additions & 1 deletion spec/linters/migration/no_model_in_specs_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/cop_helper'
require 'rubocop/config'
Expand Down Expand Up @@ -71,4 +70,31 @@

expect(result.size).to eq(0)
end

it 'registers an offense for model usage via let variable' do
result = inspect_source(<<~RUBY)
let(:annotation) { VCAP::CloudController::IsolationSegmentAnnotationModel }
annotation.create(resource_guid: 'x', key_name: 'k', value: 'v')
RUBY

expect(result.size).to eq(1)
end

it 'registers an offense for model usage via let! variable' do
result = inspect_source(<<~RUBY)
let!(:label) { VCAP::CloudController::IsolationSegmentLabelModel }
label.where(resource_guid: 'x').count
RUBY

expect(result.size).to eq(1)
end

it 'does not register an offense for let variable not holding a model' do
result = inspect_source(<<~RUBY)
let(:guid) { SecureRandom.uuid }
guid.upcase
RUBY

expect(result.size).to eq(0)
end
end
100 changes: 100 additions & 0 deletions spec/migration_spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
require 'rubygems'
require 'bundler/setup'
require 'tmpdir'
require 'fileutils'

if ENV['COVERAGE']
require 'simplecov'
SimpleCov.start
end

$LOAD_PATH.push(File.expand_path(File.join(__dir__, '..', 'app')))
$LOAD_PATH.push(File.expand_path(File.join(__dir__, '..', 'lib')))

ENV['BOOTSNAP_CACHE_DIR'] ||= File.expand_path('../tmp/bootsnap-cache', __dir__)
require 'bootsnap/setup'
require 'active_support/all'
require 'steno/steno'
require 'sequel/plugins/microsecond_timestamp_precision'

module VCAP
module CloudController
# Stub Config so migrations that call Config.config&.get(...) don't fail.
# The safe-navigation operator (&.) means nil is fine here.
class Config
def self.config
nil
end

def get(*_args); end
end
end
end

require 'cloud_controller/db'
Sequel.default_timezone = :utc
require 'cloud_controller/db_migrator'
require 'cloud_controller/database_parts_parser'
require 'support/bootstrap/db_connection_string'
require 'support/table_truncator'
require 'support/referential_integrity'
require 'support/matchers/be_a_guid'
require 'support/matchers/have_queried_db_times'
require 'support/and_record_arguments'

# Establish DB connection (sets Sequel::Model.db) without loading CC models.
connection_string = DbConnectionString.new.to_s
db_config = {
database: VCAP::CloudController::DatabasePartsParser.database_parts_from_connection(connection_string),
pool_timeout: 10,
read_timeout: 3600,
connection_validation_timeout: 3600,
max_connections: 42
}
VCAP::CloudController::DB.connect(db_config, Logger.new(nil))
Sequel.extension :migration

# Lightweight truncation that skips seed re-seeding (migration specs don't use seeds).
module MigrationSpecTruncation
WRITE_REGEX = /\b(?:INSERT INTO|UPDATE|DELETE FROM|TRUNCATE TABLE|TRUNCATE)\s+(\S+)/i

def self.cleanly(db)
tables_written = Set.new

logger = Object.new
%i[info warn debug error fatal].each do |level|
logger.define_singleton_method(level) { |msg| MigrationSpecTruncation.capture(msg, tables_written) }
end

db.loggers << logger
begin
yield
ensure
db.loggers.delete(logger)
all_tables = TableTruncator.isolated_tables(db)
tables = tables_written.to_a & all_tables
TableTruncator.new(db, tables).truncate_tables unless tables.empty?
end
end

def self.capture(msg, tables_written)
return unless msg =~ WRITE_REGEX

target = ::Regexp.last_match(1).delete('`"')
tables_written << target.to_sym unless target.include?('.')
end
end

RSpec.configure do |config|
config.before(:all, type: :migration) do
skip 'Skipped due to NO_DB_MIGRATION env variable being set' if ENV['NO_DB_MIGRATION']
end

config.around(type: :migration) do |example|
if example.metadata[:isolation] == :truncation
MigrationSpecTruncation.cleanly(Sequel::Model.db) { example.run }
else
Sequel::Model.db.transaction(rollback: :always, auto_savepoint: true) { example.run }
end
end
end
172 changes: 58 additions & 114 deletions spec/migrations/20190712210940_backfill_status_for_deployments_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'spec_helper'
require 'migration_spec_helper'

RSpec.describe 'backfill status_value for deployments', isolation: :truncation, type: :migration do
let(:db) { Sequel::Model.db }
let(:tmp_migrations_dir) { Dir.mktmpdir }

before do
Expand All @@ -10,119 +11,62 @@
)
end

let(:app) { create(:app_model) }

it 'backfills status_value based on deployment state' do
# Create all deployment variations
deployment_deployed = VCAP::CloudController::DeploymentModel.create(
guid: 'with-state-deployed',
state: VCAP::CloudController::DeploymentModel::DEPLOYED_STATE,
app: app,
original_web_process_instance_count: 1
)

deployment_canceled = VCAP::CloudController::DeploymentModel.create(
guid: 'with-state-canceled',
state: VCAP::CloudController::DeploymentModel::CANCELED_STATE,
app: app,
original_web_process_instance_count: 1
)

deployment_failed = VCAP::CloudController::DeploymentModel.create(
guid: 'with-state-failed',
state: 'FAILED',
app: app,
original_web_process_instance_count: 1
)

deployment_deploying = VCAP::CloudController::DeploymentModel.create(
guid: 'with-state-deploying',
state: VCAP::CloudController::DeploymentModel::DEPLOYING_STATE,
app: app,
original_web_process_instance_count: 1
)

deployment_canceling = VCAP::CloudController::DeploymentModel.create(
guid: 'with-state-canceling',
state: VCAP::CloudController::DeploymentModel::CANCELING_STATE,
app: app,
original_web_process_instance_count: 1
)

deployment_failing = VCAP::CloudController::DeploymentModel.create(
guid: 'with-state-failing',
state: 'FAILING',
app: app,
original_web_process_instance_count: 1
)

deployment_with_existing_status = VCAP::CloudController::DeploymentModel.create(
guid: 'with-existing-status',
state: VCAP::CloudController::DeploymentModel::DEPLOYED_STATE,
status_value: 'foo',
status_reason: 'bar',
app: app,
original_web_process_instance_count: 1
)

deployment_failing_with_reason = VCAP::CloudController::DeploymentModel.create(
guid: 'failing-with-reason',
state: 'FAILING',
status_value: 'foo',
status_reason: 'bar',
app: app,
original_web_process_instance_count: 1
)

# Run migration once
Sequel::Migrator.run(VCAP::CloudController::DeploymentModel.db, tmp_migrations_dir, table: :my_fake_table)

# Test: DEPLOYED state -> FINALIZED status
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_deployed.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYED_STATE)
expect(deployment.status_value).to eq(VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE)
expect(deployment.status_reason).to be_nil

# Test: CANCELED state -> FINALIZED status
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_canceled.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::CANCELED_STATE)
expect(deployment.status_value).to eq(VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE)
expect(deployment.status_reason).to be_nil

# Test: FAILED state -> DEPLOYED state + FINALIZED status
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_failed.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYED_STATE)
expect(deployment.status_value).to eq(VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE)
expect(deployment.status_reason).to be_nil

# Test: DEPLOYING state -> DEPLOYING status
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_deploying.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYING_STATE)
expect(deployment.status_value).to eq('DEPLOYING')
expect(deployment.status_reason).to be_nil

# Test: CANCELING state -> DEPLOYING status
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_canceling.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::CANCELING_STATE)
expect(deployment.status_value).to eq('DEPLOYING')
expect(deployment.status_reason).to be_nil

# Test: FAILING state -> DEPLOYING state + DEPLOYING status
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_failing.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYING_STATE)
expect(deployment.status_value).to eq('DEPLOYING')
expect(deployment.status_reason).to be_nil

# Test: existing status_value is not reset
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_with_existing_status.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYED_STATE)
expect(deployment.status_value).to eq('foo')
expect(deployment.status_reason).to eq('bar')

# Test: existing status_reason is preserved
deployment = VCAP::CloudController::DeploymentModel.where(guid: deployment_failing_with_reason.guid).first
expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYING_STATE)
expect(deployment.status_value).to eq('DEPLOYING')
expect(deployment.status_reason).to eq('bar')
now = Time.now.utc
app_guid = SecureRandom.uuid
db[:apps].insert(guid: app_guid, name: 'app', created_at: now, updated_at: now)

db[:deployments].insert(guid: 'with-state-deployed', app_guid: app_guid, state: 'DEPLOYED', original_web_process_instance_count: 1, created_at: now, updated_at: now)
db[:deployments].insert(guid: 'with-state-canceled', app_guid: app_guid, state: 'CANCELED', original_web_process_instance_count: 1, created_at: now, updated_at: now)
db[:deployments].insert(guid: 'with-state-failed', app_guid: app_guid, state: 'FAILED', original_web_process_instance_count: 1, created_at: now, updated_at: now)
db[:deployments].insert(guid: 'with-state-deploying', app_guid: app_guid, state: 'DEPLOYING', original_web_process_instance_count: 1, created_at: now, updated_at: now)
db[:deployments].insert(guid: 'with-state-canceling', app_guid: app_guid, state: 'CANCELING', original_web_process_instance_count: 1, created_at: now, updated_at: now)
db[:deployments].insert(guid: 'with-state-failing', app_guid: app_guid, state: 'FAILING', original_web_process_instance_count: 1, created_at: now, updated_at: now)
db[:deployments].insert(guid: 'with-existing-status', app_guid: app_guid, state: 'DEPLOYED', status_value: 'foo', status_reason: 'bar', original_web_process_instance_count: 1,
created_at: now, updated_at: now)
db[:deployments].insert(guid: 'failing-with-reason', app_guid: app_guid, state: 'FAILING', status_value: 'foo', status_reason: 'bar', original_web_process_instance_count: 1,
created_at: now, updated_at: now)

Sequel::Migrator.run(db, tmp_migrations_dir, table: :my_fake_table)

deployment = db[:deployments].where(guid: 'with-state-deployed').first
expect(deployment[:state]).to eq('DEPLOYED')
expect(deployment[:status_value]).to eq('FINALIZED')
expect(deployment[:status_reason]).to be_nil

deployment = db[:deployments].where(guid: 'with-state-canceled').first
expect(deployment[:state]).to eq('CANCELED')
expect(deployment[:status_value]).to eq('FINALIZED')
expect(deployment[:status_reason]).to be_nil

deployment = db[:deployments].where(guid: 'with-state-failed').first
expect(deployment[:state]).to eq('DEPLOYED')
expect(deployment[:status_value]).to eq('FINALIZED')
expect(deployment[:status_reason]).to be_nil

deployment = db[:deployments].where(guid: 'with-state-deploying').first
expect(deployment[:state]).to eq('DEPLOYING')
expect(deployment[:status_value]).to eq('DEPLOYING')
expect(deployment[:status_reason]).to be_nil

deployment = db[:deployments].where(guid: 'with-state-canceling').first
expect(deployment[:state]).to eq('CANCELING')
expect(deployment[:status_value]).to eq('DEPLOYING')
expect(deployment[:status_reason]).to be_nil

deployment = db[:deployments].where(guid: 'with-state-failing').first
expect(deployment[:state]).to eq('DEPLOYING')
expect(deployment[:status_value]).to eq('DEPLOYING')
expect(deployment[:status_reason]).to be_nil

deployment = db[:deployments].where(guid: 'with-existing-status').first
expect(deployment[:state]).to eq('DEPLOYED')
expect(deployment[:status_value]).to eq('foo')
expect(deployment[:status_reason]).to eq('bar')

deployment = db[:deployments].where(guid: 'failing-with-reason').first
expect(deployment[:state]).to eq('DEPLOYING')
expect(deployment[:status_value]).to eq('DEPLOYING')
expect(deployment[:status_reason]).to eq('bar')
end
end
Loading
Loading