Skip to content

Commit

Permalink
feat(pacts for verification): allow all versions for a particular tag…
Browse files Browse the repository at this point in the history
… to be verified (eg. all prod versions of a mobile consumer)
  • Loading branch information
bethesque committed Jan 30, 2020
1 parent cd63be0 commit e16feef
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class VerifiablePactsJSONQuerySchema
optional(:consumerVersionSelectors).each do
schema do
required(:tag).filled(:str?)
required(:latest).filled(included_in?: [true])
optional(:latest).filled(included_in?: [true, false])
end
end
optional(:includePendingStatus).filled(included_in?: [true, false])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class VerifiablePactsQuerySchema
optional(:consumer_version_selectors).each do
schema do
required(:tag).filled(:str?)
required(:latest).filled(included_in?: ["true"])
optional(:latest).filled(included_in?: ["true", "false"])
end
end
optional(:include_pending_status).filled(included_in?: ["true", "false"])
Expand Down
53 changes: 53 additions & 0 deletions lib/pact_broker/pacts/pact_publication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,63 @@ class PactPublication < Sequel::Model(:pact_publications)
dataset_module do
include PactBroker::Repositories::Helpers

def remove_overridden_revisions
join(:latest_pact_publication_ids_for_consumer_versions, { Sequel[:lp][:pact_publication_id] => Sequel[:pact_publications][:id] }, { table_alias: :lp})
end

def join_consumer_versions(table_alias = :cv)
join(:versions, { Sequel[:pact_publications][:consumer_version_id] => Sequel[table_alias][:id] }, { table_alias: table_alias })
end

def join_consumer_version_tags(table_alias = :ct)
join(:tags, { Sequel[table_alias][:version_id] => Sequel[:pact_publications][:consumer_version_id]}, { table_alias: table_alias })
end

def join_consumer_version_tags_with_names(consumer_version_tag_names)
join(:tags, {
Sequel[:ct][:version_id] => Sequel[:pact_publications][:consumer_version_id],
Sequel[:ct][:name] => consumer_version_tag_names
}, {
table_alias: :ct
})
end

def join_providers(table_alias = :providers)
join(:pacticipants, { Sequel[:pact_publications][:provider_id] => Sequel[table_alias][:id] }, { table_alias: table_alias })
end

def join_consumers(table_alias = :consumers)
join(:pacticipants, { Sequel[:pact_publications][:consumer_id] => Sequel[table_alias][:id] }, { table_alias: table_alias })
end

def join_pact_versions
join(:pact_versions, { Sequel[:pact_publications][:pact_version_id] => Sequel[:pact_versions][:id] })
end

def eager_load_pact_versions
eager(:pact_versions)
end

def tag tag_name
filter = name_like(Sequel.qualify(:tags, :name), tag_name)
join(:tags, {version_id: :consumer_version_id}).where(filter)
end

def provider_name_like(name)
where(name_like(Sequel[:providers][:name], name))
end

def consumer_version_tag(tag)
where(Sequel[:ct][:name] => tag)
end

def order_by_consumer_name
order_append_ignore_case(Sequel[:consumers][:name])
end

def order_by_consumer_version_order
order_append(Sequel[:cv][:order])
end
end

def before_create
Expand Down
108 changes: 81 additions & 27 deletions lib/pact_broker/pacts/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
require 'pact_broker/matrix/head_row'
require 'pact_broker/pacts/latest_pact_publication_id_for_consumer_version'
require 'pact_broker/pacts/verifiable_pact'
require 'pact_broker/repositories/helpers'

module PactBroker
module Pacts
class Repository

include PactBroker::Logging
include PactBroker::Repositories
include PactBroker::Repositories::Helpers

def create params
pact_version = find_or_create_pact_version(
Expand Down Expand Up @@ -125,36 +127,60 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil
end
end

def find_all_pact_versions_for_provider_with_tags provider_name, consumer_version_tag_names
provider = pacticipant_repository.find_by_name(provider_name)

PactPublication
.select_all_qualified
.select_append(Sequel[:cv][:order].as(:consumer_version_order))
.remove_overridden_revisions
.join_consumer_versions(:cv)
.join_consumer_version_tags_with_names(consumer_version_tag_names)
.where(provider: provider)
.eager(:consumer)
.eager(:consumer_version)
.eager(:provider)
.eager(:pact_version)
.all
.group_by(&:pact_version_id)
.values
.collect{ | pacts| pacts.sort_by{|pact| pact.values.fetch(:consumer_version_order) }.last }
.collect(&:to_domain)
end

# To find the work in progress pacts for this verification execution:
# For each provider tag that will be applied to this verification result (usually there will just be one, but
# we have to allow for multiple tags),
# find the head pacts (the pacts that are the latest for their tag) that have been successfully
# verified against the provider tag.
# Then, find all the head pacts, and remove the ones that have been successfully verified by ALL
# of the provider tags supplied.
# of the provider tags supplied, and the ones that were published before the include_wip_pacts_since date.
# Then, for all of the head pacts that are remaining (these are the WIP ones) work out which
# provider tags they are pending for.
# Don't include pact publications that were created
# Don't include pact publications that were created before the provider tag was first used
# (that is, before the provider's git branch was created).
def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [], options = {}
# TODO not sure about this
return [] if provider_tags_names.empty?

provider = pacticipant_repository.find_by_name(provider_name)

# Hash of provider tag names => list of head pacts
successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags_names, options)
# Hash of provider tag name => list of head pacts that have been successfully verified by that tag
successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider.id, provider_tags_names, options)
# Create hash of provider tag name => list of pact publication ids
successfully_verified_head_pact_publication_ids_for_each_provider_tag = successfully_verified_head_pacts_for_provider_tags.each_with_object({}) do | (provider_tag_name, head_pacts), hash |
hash[provider_tag_name] = head_pacts.collect(&:id)
hash[provider_tag_name] = head_pacts.collect(&:id).uniq
end

# list of pact_publication_ids that are NOT work in progress
head_pact_publication_ids_successully_verified_by_all_provider_tags = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&)
# list of head pact_publication_ids that are NOT work in progress because they've been verified by all of the provider version tags supplied
non_wip_pact_publication_ids = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&)

pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(
provider_name,
head_pact_publication_ids_successully_verified_by_all_provider_tags,
wip_pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(
provider.id,
non_wip_pact_publication_ids,
options)

pacts = AllPactPublications.where(id: pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order)
wip_pacts = AllPactPublications.where(id: wip_pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order)

# The first instance (by date) of each provider tag with that name
provider_tag_collection = PactBroker::Domain::Tag
Expand All @@ -166,7 +192,7 @@ def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [],
.where(name: provider_tags_names)
.all

pacts.collect do | pact|
wip_pacts.collect do | pact|
pending_tag_names = find_provider_tags_for_which_pact_publication_id_is_pending(pact, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
pre_existing_tag_names = find_provider_tag_names_that_were_first_used_before_pact_published(pact, provider_tag_collection)

Expand Down Expand Up @@ -313,13 +339,33 @@ def find_previous_pacts pact

# Returns a list of Domain::Pact objects the represent pact publications
def find_for_verification(provider_name, consumer_version_selectors)
find_pacts_for_which_the_latest_version_or_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors) +
find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors)
end

private

def find_pacts_for_which_the_latest_version_or_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors)
# The tags for which only the latest version is specified
latest_tags = consumer_version_selectors.any? ?
consumer_version_selectors.select(&:latest).collect(&:tag) :
nil

find_latest_pact_versions_for_provider(provider_name, latest_tags)
end

private
def find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors)
# The tags for which all versions are specified
all_tags = consumer_version_selectors.any? ?
consumer_version_selectors.reject(&:latest).collect(&:tag) :
nil

if all_tags
find_all_pact_versions_for_provider_with_tags(provider_name, all_tags)
else
[]
end
end

def find_previous_distinct_pact_by_sha pact
current_pact_content_sha =
Expand Down Expand Up @@ -389,27 +435,35 @@ def to_datetime string_or_datetime
end
end

def find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(provider_name, pact_publication_ids_successfully_verified_by_all_provider_tags, options)
def find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(provider_id, pact_publication_ids_successfully_verified_by_all_provider_tags, options)
# Exclude the head pacts that have been successfully verified by all the specified provider tags
pact_publication_ids = LatestTaggedPactPublications
.provider(provider_name)
.exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags)
LatestTaggedPactPublications
.where(provider_id: provider_id)
.where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since)))
.exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags)
.select_for_subquery(:id)
end

# Find the head pacts that have been successfully verified by a provider version with the specified tags
# Returns a Hash of provider_tag => LatestTaggedPactPublications with only id and tag_name populated
def find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options)
# Find the head pacts that have been successfully verified by a provider version with the specified
# provider version tags.
# Returns a Hash of provider_tag => LatestTaggedPactPublications with only pact publication id and tag_name populated
# This is the list of pacts we are EXCLUDING from the WIP list because they have already been verified successfully
def find_successfully_verified_head_pacts_by_provider_tag(provider_id, provider_tags, options)
provider_tags.compact.each_with_object({}) do | provider_tag, hash |
verifications_join = {
pact_version_id: :pact_version_id,
Sequel[:verifications][:success] => true,
Sequel[:verifications][:provider_id] => provider_id
}
tags_join = {
Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id],
Sequel[:provider_tags][:name] => provider_tag
}
head_pacts = LatestTaggedPactPublications
.join(:verifications, { pact_version_id: :pact_version_id })
.join(:tags, { Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id] }, {table_alias: :provider_tags})
.where(Sequel[:provider_tags][:name] => provider_tag)
.provider(provider_name)
.where(Sequel[:verifications][:success] => true)
.or(Sequel.lit('latest_tagged_pact_publications.created_at < ?', options.fetch(:include_wip_pacts_since)))
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id), :tag_name)
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id))
.join(:verifications, verifications_join)
.join(:tags, tags_join, { table_alias: :provider_tags } )
.where(Sequel[:latest_tagged_pact_publications][:provider_id] => provider_id)
.all
hash[provider_tag] = head_pacts
end
Expand Down
4 changes: 4 additions & 0 deletions lib/pact_broker/repositories/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def order_ignore_case column_name = :name
order(Sequel.function(:lower, column_name))
end

def order_append_ignore_case column_name = :name
order_append(Sequel.function(:lower, column_name))
end

def mysql?
Sequel::Model.db.adapter_scheme.to_s =~ /mysql/
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module Contracts
}]
end

it { is_expected.to have_key(:consumerVersionSelectors) }
it { is_expected.to be_empty }
end

context "when includeWipPactsSince key exists" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module Contracts
}]
end

it { is_expected.to have_key(:consumer_version_selectors) }
it { is_expected.to be_empty }
end

context "when include_wip_pacts_since key exists" do
Expand Down
67 changes: 55 additions & 12 deletions spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,35 @@ def find_by_consumer_version_number(consumer_version_number)
subject.find{ |pact| pact.consumer_version_number == consumer_version_number }
end

def find_by_consumer_name_and_consumer_version_number(consumer_name, consumer_version_number)
subject.find{ |pact| pact.consumer_name == consumer_name && pact.consumer_version_number == consumer_version_number }
end

before do
td.create_pact_with_hierarchy("Foo", "bar-latest-prod", "Bar")
td.create_pact_with_hierarchy("Foo", "foo-latest-prod-version", "Bar")
.create_consumer_version_tag("prod")
.create_consumer_version("not-latest-dev", tag_names: ["dev"])
.create_consumer_version("not-latest-dev-version", tag_names: ["dev"])
.comment("next pact not selected")
.create_pact
.create_consumer_version("bar-latest-dev", tag_names: ["dev"])
.create_consumer_version("foo-latest-dev-version", tag_names: ["dev"])
.create_pact
.create_consumer("Baz")
.create_consumer_version("baz-latest-dev", tag_names: ["dev"])
.create_consumer_version("baz-latest-dev-version", tag_names: ["dev"])
.create_pact
end

subject { Repository.new.find_for_verification("Bar", consumer_version_selectors) }

context "when there are no selectors" do
let(:consumer_version_selectors) { [] }

it "returns the latest pact for each consumer" do
expect(subject.size).to eq 2
expect(find_by_consumer_name_and_consumer_version_number("Foo", "foo-latest-dev-version")).to_not be nil
expect(find_by_consumer_name_and_consumer_version_number("Baz", "baz-latest-dev-version")).to_not be nil
end
end

context "when consumer tag names are specified" do
let(:pact_selector_1) { double('selector', tag: 'dev', latest: true) }
let(:pact_selector_2) { double('selector', tag: 'prod', latest: true) }
Expand All @@ -34,29 +48,58 @@ def find_by_consumer_version_number(consumer_version_number)
end

it "returns the latest pact with the specified tags for each consumer" do
expect(find_by_consumer_version_number("bar-latest-prod")).to_not be nil
expect(find_by_consumer_version_number("bar-latest-dev")).to_not be nil
expect(find_by_consumer_version_number("baz-latest-dev")).to_not be nil
expect(find_by_consumer_version_number("foo-latest-prod-version")).to_not be nil
expect(find_by_consumer_version_number("foo-latest-dev-version")).to_not be nil
expect(find_by_consumer_version_number("baz-latest-dev-version")).to_not be nil
expect(subject.size).to eq 3
end

it "sets the latest_consumer_version_tag_names" do
expect(find_by_consumer_version_number("bar-latest-prod").tag).to eq 'prod'
expect(find_by_consumer_version_number("foo-latest-prod-version").tag).to eq 'prod'
end

context "when all versions with a given tag are requested" do
before do
td.create_pact_with_hierarchy("Foo2", "prod-version-1", "Bar2")
.create_consumer_version_tag("prod")
.create_consumer_version("not-prod-version", tag_names: %w[master])
.create_pact
.create_consumer_version("prod-version-2", tag_names: %w[prod])
.create_pact
end

let(:consumer_version_selectors) { [pact_selector_1] }
let(:pact_selector_1) { double('selector', tag: 'prod', latest: nil) }

subject { Repository.new.find_for_verification("Bar2", consumer_version_selectors) }

it "returns all the versions with the specified tag" do
expect(subject.size).to be 2
expect(find_by_consumer_version_number("prod-version-1")).to_not be nil
expect(find_by_consumer_version_number("prod-version-2")).to_not be nil
end

it "dedupes them to ensure that each pact version is only verified once" do
td.create_consumer_version("prod-version-3", tag_names: %w[prod])
.republish_same_pact
expect(subject.size).to be 2
expect(subject.collect(&:consumer_version_number)).to eq %w[prod-version-1 prod-version-3]
end
end
end

context "when no selectors are specified" do
let(:consumer_version_selectors) { [] }

it "returns the latest pact for each provider" do
expect(find_by_consumer_version_number("bar-latest-dev")).to_not be nil
expect(find_by_consumer_version_number("baz-latest-dev")).to_not be nil
expect(find_by_consumer_version_number("foo-latest-dev-version")).to_not be nil
expect(find_by_consumer_version_number("baz-latest-dev-version")).to_not be nil
expect(subject.size).to eq 2
end

it "does not set the tag name" do
expect(find_by_consumer_version_number("bar-latest-dev").tag).to be nil
expect(find_by_consumer_version_number("bar-latest-dev").overall_latest?).to be true
expect(find_by_consumer_version_number("foo-latest-dev-version").tag).to be nil
expect(find_by_consumer_version_number("foo-latest-dev-version").overall_latest?).to be true
end
end
end
Expand Down

0 comments on commit e16feef

Please sign in to comment.