gitlab-ce/spec/scripts/click_house/schema_validator_spec.rb

457 lines
16 KiB
Ruby

# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../scripts/click_house/schema_validator'
RSpec.describe ClickHouse::SchemaValidator, feature_category: :database do
describe '.validate!' do
let(:schema_filename) { 'db/click_house/main.sql' }
before do
# Suppress puts output during tests
allow($stdout).to receive(:puts)
end
context 'when migration fails' do
before do
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(false)
end
it 'returns false' do
expect(described_class.validate!).to be false
end
it 'prints error message' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...')
expect($stdout).to receive(:puts).with('ERROR: ClickHouse migration failed')
described_class.validate!
end
it 'does not check for schema changes' do
expect(described_class).not_to receive(:execute_git_diff)
described_class.validate!
end
end
context 'when migration succeeds' do
before do
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(true)
end
context 'when execute_git_diff returns nil (git command failed)' do
before do
allow(described_class).to receive(:execute_git_diff).and_return(nil)
end
it 'returns false' do
expect(described_class.validate!).to be false
end
it 'prints expected messages' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...')
expect($stdout).to receive(:puts).with('Checking for schema changes...')
described_class.validate!
end
end
context 'when execute_git_diff succeeds and git command is successful' do
before do
allow(described_class).to receive(:git_command_successful?).and_return(true)
end
context 'when schema has no changes' do
before do
allow(described_class).to receive(:execute_git_diff).and_return('')
end
it 'returns true' do
expect(described_class.validate!).to be true
end
it 'prints success message' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...')
expect($stdout).to receive(:puts).with('Checking for schema changes...')
expect($stdout).to receive(:puts).with('Schema is up to date - no changes detected')
described_class.validate!
end
end
context 'when schema has changes' do
let(:git_diff_output) { "#{schema_filename}\nother_file.rb" }
let(:schema_diff) { "- old line\n+ new line" }
before do
allow(described_class).to receive(:execute_git_diff).and_return(git_diff_output)
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return(schema_diff)
end
it 'returns false' do
expect(described_class.validate!).to be false
end
it 'prints change detection messages' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...')
expect($stdout).to receive(:puts).with('Checking for schema changes...')
expect($stdout).to receive(:puts).with('Schema has uncommitted changes after migration')
expect($stdout).to receive(:puts).with("Changes detected in: #{schema_filename}")
expect($stdout).to receive(:puts).with('Diff output:')
expect($stdout).to receive(:puts).with(schema_diff)
described_class.validate!
end
end
context 'when git diff output contains schema filename as substring' do
before do
allow(described_class).to receive(:execute_git_diff)
.and_return("some_other_#{schema_filename}_backup")
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return('changes')
end
it 'correctly identifies schema changes (substring match)' do
expect(described_class.validate!).to be false
end
end
context 'when git diff output contains exact schema filename' do
before do
allow(described_class).to receive(:execute_git_diff).and_return(schema_filename)
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return('diff content')
end
it 'correctly identifies schema changes' do
expect(described_class.validate!).to be false
end
end
context 'when git diff output contains schema filename with newlines' do
before do
allow(described_class).to receive(:execute_git_diff).and_return("some_file.txt\n#{schema_filename}\n")
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return('diff content')
end
it 'correctly identifies schema changes' do
expect(described_class.validate!).to be false
end
end
end
end
end
describe '.execute_git_diff' do
let(:schema_filename) { 'db/click_house/main.sql' }
let(:git_command) { "git diff --name-only -- #{schema_filename}" }
before do
allow($stdout).to receive(:puts)
end
context 'when git command succeeds' do
let(:git_output) { "#{schema_filename}\nother_file.rb" }
before do
allow(described_class).to receive(:`).with(git_command).and_return(git_output)
allow(described_class).to receive(:git_command_successful?).and_return(true)
end
it 'returns the git output' do
expect(described_class.execute_git_diff).to eq(git_output)
end
it 'does not print error message' do
expect($stdout).not_to receive(:puts).with('ERROR: Git diff command failed')
described_class.execute_git_diff
end
end
context 'when git command fails' do
before do
allow(described_class).to receive(:`).with(git_command).and_return('')
allow(described_class).to receive(:git_command_successful?).and_return(false)
end
it 'returns nil' do
expect(described_class.execute_git_diff).to be_nil
end
it 'does not print error message' do
expect($stdout).not_to receive(:puts).with('ERROR: Git diff command failed')
described_class.execute_git_diff
end
end
context 'when git command returns empty output but succeeds' do
before do
allow(described_class).to receive(:`).with(git_command).and_return('')
allow(described_class).to receive(:git_command_successful?).and_return(true)
end
it 'returns empty string' do
expect(described_class.execute_git_diff).to eq('')
end
end
context 'when git command returns whitespace' do
before do
allow(described_class).to receive(:`).with(git_command).and_return(" \n \t ")
allow(described_class).to receive(:git_command_successful?).and_return(true)
end
it 'returns the whitespace string' do
expect(described_class.execute_git_diff).to eq(" \n \t ")
end
end
end
describe '.git_command_successful?' do
context 'when $? indicates success' do
before do
# Simulate successful command execution
`true` # This sets $? to success
end
it 'returns true' do
expect(described_class.git_command_successful?).to be true
end
end
context 'when $? indicates failure' do
before do
# Simulate failed command execution
`false` # This sets $? to failure
end
it 'returns false' do
expect(described_class.git_command_successful?).to be false
end
end
end
describe 'constants' do
it 'defines the correct schema filename' do
expect(described_class::SCHEMA_FILENAME).to eq('db/click_house/main.sql')
end
end
describe 'integration scenarios' do
let(:schema_filename) { 'db/click_house/main.sql' }
before do
allow($stdout).to receive(:puts)
end
context 'when full success flow - no schema changes' do
before do
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(true)
allow(described_class).to receive_messages(
execute_git_diff: '',
git_command_successful?: true
)
end
it 'executes all steps in correct order and returns true' do
expect(described_class).to receive(:system).with('bundle exec rake gitlab:clickhouse:migrate:main').ordered
expect(described_class).to receive(:execute_git_diff).ordered
result = described_class.validate!
expect(result).to be true
end
it 'prints expected success messages' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...').ordered
expect($stdout).to receive(:puts).with('Checking for schema changes...').ordered
expect($stdout).to receive(:puts).with('Schema is up to date - no changes detected').ordered
described_class.validate!
end
end
context 'when full failure flow - schema has changes' do
let(:schema_diff) { "- CREATE TABLE old\n+ CREATE TABLE new" }
before do
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(true)
allow(described_class).to receive_messages(
execute_git_diff: schema_filename,
git_command_successful?: true
)
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return(schema_diff)
end
it 'executes all steps and returns false' do
expect(described_class).to receive(:system).with('bundle exec rake gitlab:clickhouse:migrate:main').ordered
expect(described_class).to receive(:execute_git_diff).ordered
expect(described_class).to receive(:`).with("git diff -- #{schema_filename}").ordered
result = described_class.validate!
expect(result).to be false
end
it 'prints expected failure messages' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...').ordered
expect($stdout).to receive(:puts).with('Checking for schema changes...').ordered
expect($stdout).to receive(:puts).with('Schema has uncommitted changes after migration').ordered
expect($stdout).to receive(:puts).with("Changes detected in: #{schema_filename}").ordered
expect($stdout).to receive(:puts).with('Diff output:').ordered
expect($stdout).to receive(:puts).with(schema_diff).ordered
described_class.validate!
end
end
context 'when migration failure flow' do
before do
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(false)
end
it 'stops at migration step and returns false' do
expect(described_class).to receive(:system).with('bundle exec rake gitlab:clickhouse:migrate:main').ordered
expect(described_class).not_to receive(:execute_git_diff)
result = described_class.validate!
expect(result).to be false
end
it 'prints migration failure messages' do
expect($stdout).to receive(:puts).with('Running ClickHouse migrations...').ordered
expect($stdout).to receive(:puts).with('ERROR: ClickHouse migration failed').ordered
described_class.validate!
end
end
context 'when git diff execution failure flow' do
before do
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(true)
allow(described_class).to receive(:execute_git_diff).and_return(nil)
end
it 'stops at git diff step and returns false' do
expect(described_class).to receive(:system).with('bundle exec rake gitlab:clickhouse:migrate:main').ordered
expect(described_class).to receive(:execute_git_diff).ordered
expect(described_class).not_to receive(:git_command_successful?)
result = described_class.validate!
expect(result).to be false
end
end
end
describe 'edge cases and boundary conditions' do
let(:schema_filename) { 'db/click_house/main.sql' }
before do
allow($stdout).to receive(:puts)
allow(described_class).to receive(:system)
.with('bundle exec rake gitlab:clickhouse:migrate:main')
.and_return(true)
allow(described_class).to receive(:git_command_successful?).and_return(true)
end
context 'when schema filename appears as exact match in a list' do
before do
allow(described_class).to receive(:execute_git_diff).and_return("file1.txt\n#{schema_filename}\nfile2.txt")
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return('changes')
end
it 'correctly identifies schema changes' do
expect(described_class.validate!).to be false
end
end
context 'when multiple files contain schema filename as substring' do
before do
allow(described_class).to receive(:execute_git_diff)
.and_return("backup_#{schema_filename}\n#{schema_filename}_old\nrandom_file.txt")
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return('changes')
end
it 'correctly identifies schema changes due to substring matches' do
expect(described_class.validate!).to be false
end
end
context 'when output contains only unrelated files' do
before do
allow(described_class).to receive(:execute_git_diff)
.and_return("totally_different_file.txt\nanother_file.rb\nthird_file.py")
end
it 'correctly identifies no schema changes' do
expect(described_class.validate!).to be true
end
end
context 'when output is nil (execute_git_diff failed)' do
before do
allow(described_class).to receive(:execute_git_diff).and_return(nil)
end
it 'returns false immediately' do
expect(described_class.validate!).to be false
end
end
context 'when schema filename is empty string (edge case)' do
before do
stub_const('ClickHouse::SchemaValidator::SCHEMA_FILENAME', '')
allow(described_class).to receive(:execute_git_diff).and_return('any_file.txt')
end
it 'handles empty schema filename gracefully' do
# With empty string, include? will always return true for any non-empty string
expect(described_class.validate!).to be false
end
end
context 'when git diff output is exactly the schema filename' do
before do
allow(described_class).to receive(:execute_git_diff).and_return(schema_filename)
allow(described_class).to receive(:`).with("git diff -- #{schema_filename}").and_return('schema changes')
end
it 'correctly identifies schema changes' do
expect(described_class.validate!).to be false
end
end
context 'when git diff output is empty string' do
before do
allow(described_class).to receive(:execute_git_diff).and_return('')
end
it 'correctly identifies no schema changes' do
expect(described_class.validate!).to be true
end
end
context 'when git diff output contains only whitespace' do
before do
allow(described_class).to receive(:execute_git_diff).and_return(" \n \t ")
end
it 'correctly identifies no schema changes' do
expect(described_class.validate!).to be true
end
end
end
end