Commit 08522414 authored by Lin Jen-Shin's avatar Lin Jen-Shin 🍀

Always use `attribute` to define the product

The idea is that we define the attributes on the factory,
and make them lazily loaded. The factory is free to use them,
and so is the product. The product will just load everything
from the factory.

This eliminate the difference between factory and product.
The product is now just a delegation to the factory, without
having `fabricate!` and other methods.
parent 054d900e
Pipeline #33450913 failed with stages
in 93 minutes and 53 seconds
......@@ -38,7 +38,7 @@ module QA
module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base'
autoload :Dependency, 'qa/factory/dependency'
autoload :Attributes, 'qa/factory/attributes'
autoload :Product, 'qa/factory/product'
module Resource
......
# frozen_string_literal: true
module QA
module Factory
module Attributes
NoValueError = Class.new(RuntimeError)
private
def populate_attribute(name, block)
value = attribute_value(name, block)
raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value
value
end
def attribute_value(name, block)
api_resource&.dig(name) || instance_exec(&block)
end
end
end
end
......@@ -10,8 +10,7 @@ module QA
include ApiFabricator
extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes
def_delegators :evaluator, :attribute
def fabricate!(*_args)
raise NotImplementedError
......@@ -52,10 +51,6 @@ module QA
def self.do_fabricate!(factory:, prepare_block:, parents: [])
prepare_block.call(factory) if prepare_block
dependencies.each do |signature|
Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
end
resource_web_url = yield
Factory::Product.populate!(factory, resource_web_url)
......@@ -85,30 +80,37 @@ module QA
end
private_class_method :evaluator
class DSL
attr_reader :dependencies, :attributes
def self.dynamic_attributes
const_get(:DynamicAttributes)
rescue NameError
mod = const_set(:DynamicAttributes, Module.new(Attributes))
include mod
mod
end
def self.attributes_names
dynamic_attributes.instance_methods(false).grep_v(/=$/)
end
class DSL
def initialize(base)
@base = base
@dependencies = []
@attributes = []
end
def dependency(factory, as:, &block)
as.tap do |name|
@base.class_eval { attr_accessor name }
def attribute(name, &block)
@base.dynamic_attributes.module_eval do
attr_writer(name)
Dependency::Signature.new(name, factory, block).tap do |signature|
@dependencies << signature
define_method(name) do
instance_variable_get("@#{name}") ||
instance_variable_set(
"@#{name}",
populate_attribute(name, block))
end
end
end
def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature|
@attributes << signature
end
end
end
end
end
......
module QA
module Factory
class Dependency
Signature = Struct.new(:name, :factory, :block)
def initialize(caller_factory, dependency_signature)
@caller_factory = caller_factory
@dependency_signature = dependency_signature
end
def overridden?
!!@caller_factory.public_send(@dependency_signature.name)
end
def build!(parents: [])
return if overridden?
dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@dependency_signature.block&.call(factory, @caller_factory)
end
dependency.tap do |dependency|
@caller_factory.public_send("#{@dependency_signature.name}=", dependency)
end
end
end
end
end
......@@ -5,17 +5,13 @@ module QA
class Product
include Capybara::DSL
NoValueError = Class.new(RuntimeError)
attr_reader :factory, :web_url
Attribute = Struct.new(:name, :block)
def initialize(factory, web_url)
@factory = factory
@web_url = web_url
populate_attributes!
define_attributes
end
def visit!
......@@ -28,23 +24,13 @@ module QA
private
def populate_attributes!
factory.class.attributes.each do |attribute|
instance_exec(factory, attribute.block) do |factory, block|
value = attribute_value(attribute, block)
raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value
define_singleton_method(attribute.name) { value }
def define_attributes
factory.class.attributes_names.each do |attribute|
product.define_singleton_method(name) do
factory.public_send(name)
end
end
end
def attribute_value(attribute, block)
factory.api_resource&.dig(attribute.name) ||
(block && block.call(factory)) ||
(factory.respond_to?(attribute.name) && factory.public_send(attribute.name))
end
end
end
end
......@@ -2,13 +2,14 @@ module QA
module Factory
module Repository
class ProjectPush < Factory::Repository::Push
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-code'
project.description = 'Project with repository'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-code'
resource.description = 'Project with repository'
end
end
product :output
product :project
attribute :output
def initialize
@file_name = 'file.txt'
......
......@@ -2,10 +2,12 @@ module QA
module Factory
module Repository
class WikiPush < Factory::Repository::Push
dependency Factory::Resource::Wiki, as: :wiki do |wiki|
wiki.title = 'Home'
wiki.content = '# My First Wiki Content'
wiki.message = 'Update home'
attribute :wiki do
Factory::Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
end
end
def initialize
......
......@@ -5,8 +5,10 @@ module QA
attr_accessor :project, :branch_name,
:allow_to_push, :allow_to_merge, :protected
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'protected-branch-project'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
def initialize
......
......@@ -4,11 +4,11 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
product :fingerprint do |resource|
Page::Project::Settings::Repository.act do
expand_deploy_keys do |key|
key_offset = key.key_titles.index do |title|
title.text == resource.title
attribute :fingerprint do
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |key|
key_offset = key.key_titles.index do |key_title|
key_title.text == title
end
key.key_fingerprints[key_offset].text
......@@ -16,9 +16,11 @@ module QA
end
end
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy'
project.description = 'project for adding deploy key test'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-deploy'
resource.description = 'project for adding deploy key test'
end
end
def fabricate!
......
......@@ -8,8 +8,10 @@ module QA
:content,
:commit_message
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-new-file'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-new-file'
end
end
def initialize
......
......@@ -2,17 +2,19 @@ module QA
module Factory
module Resource
class Fork < Factory::Base
dependency Factory::Repository::ProjectPush, as: :push
attribute :push do
Factory::Repository::ProjectPush.fabricate!
end
dependency Factory::Resource::User, as: :user do |user|
if Runtime::Env.forker?
user.username = Runtime::Env.forker_username
user.password = Runtime::Env.forker_password
attribute :user do
Factory::Resource::User.fabricate! do |resource|
if Runtime::Env.forker?
resource.username = Runtime::Env.forker_username
resource.password = Runtime::Env.forker_password
end
end
end
product :user
def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the
# project page. The new user is registered and then signs in and a
......@@ -48,6 +50,9 @@ module QA
end
def fabricate!
push
user
visit_project_with_retry
Page::Project::Show.act { fork_project }
......
......@@ -4,7 +4,9 @@ module QA
class Group < Factory::Base
attr_accessor :path, :description
dependency Factory::Resource::Sandbox, as: :sandbox
attribute :sandbox do
Factory::Resource::Sandbox.fabricate!
end
product :id do
true # We don't retrieve the Group ID when using the Browser UI
......
......@@ -2,15 +2,18 @@ module QA
module Factory
module Resource
class Issue < Factory::Base
attr_accessor :title, :description, :project
attr_writer :description
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-for-issues'
resource.description = 'project for adding issues'
end
end
product :project
product :title
attribute :title do
Page::Project::Issue::Show.act { issue_title }
end
def fabricate!
project.visit!
......
......@@ -7,7 +7,7 @@ module QA
attr_writer :project, :cluster,
:install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
product :ingress_ip do
attribute :ingress_ip do
Page::Project::Operations::Kubernetes::Show.perform do |page|
page.ingress_ip
end
......
......@@ -12,26 +12,32 @@ module QA
:milestone,
:labels
product :project
product :source_branch
attribute :source_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-merge-request'
end
end
dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
factory.project.visit!
push.project = factory.project
push.branch_name = 'master'
push.remote_branch = factory.target_branch
attribute :target do
project.visit!
Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.branch_name = 'master'
resource.remote_branch = target_branch
end
end
dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
push.project = factory.project
push.branch_name = factory.target_branch
push.remote_branch = factory.source_branch
push.file_name = "added_file.txt"
push.file_content = "File Added"
attribute :source do
Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.branch_name = target_branch
resource.remote_branch = source_branch
resource.file_name = "added_file.txt"
resource.file_content = "File Added"
end
end
def initialize
......@@ -45,6 +51,8 @@ module QA
end
def fabricate!
target
source
project.visit!
Page::Project::Show.act { new_merge_request }
Page::MergeRequest::New.perform do |page|
......
......@@ -4,16 +4,21 @@ module QA
class MergeRequestFromFork < MergeRequest
attr_accessor :fork_branch
dependency Factory::Resource::Fork, as: :fork
attribute :fork do
Factory::Resource::Fork.fabricate!
end
dependency Factory::Repository::ProjectPush, as: :push do |push, factory|
push.project = factory.fork
push.branch_name = factory.fork_branch
push.file_name = 'file2.txt'
push.user = factory.fork.user
attribute :push do
Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = fork
resource.branch_name = fork_branch
resource.file_name = 'file2.txt'
resource.user = fork.user
end
end
def fabricate!
push
fork.visit!
Page::Project::Show.act { new_merge_request }
Page::MergeRequest::New.act { create_merge_request }
......
......@@ -7,7 +7,7 @@ module QA
class PersonalAccessToken < Factory::Base
attr_accessor :name
product :access_token do
attribute :access_token do
Page::Profile::PersonalAccessTokens.act { created_access_token }
end
......
......@@ -4,22 +4,22 @@ module QA
module Factory
module Resource
class Project < Factory::Base
attr_accessor :description
attr_reader :name
attr_writer :description
dependency Factory::Resource::Group, as: :group
attribute :group do
Factory::Resource::Group.fabricate!
end
product :group
product :name
attribute :name
product :repository_ssh_location do
attribute :repository_ssh_location do
Page::Project::Show.act do
choose_repository_clone_ssh
repository_location
end
end
product :repository_http_location do
attribute :repository_http_location do
Page::Project::Show.act do
choose_repository_clone_http
repository_location
......
......@@ -6,9 +6,11 @@ module QA
class ProjectImportedFromGithub < Resource::Project
attr_writer :personal_access_token, :github_repository_path
dependency Factory::Resource::Group, as: :group
attribute :group do
Factory::Resource::Group.fabricate!
end
product :name
attribute :name
def fabricate!
group.visit!
......
......@@ -3,11 +3,12 @@ module QA
module Resource
class ProjectMilestone < Factory::Base
attr_accessor :description
attr_reader :title
dependency Factory::Resource::Project, as: :project
attribute :project do
Factory::Resource::Project.fabricate!
end
product :title
attribute :title
def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}"
......
......@@ -6,9 +6,11 @@ module QA
class Runner < Factory::Base
attr_writer :name, :tags, :image
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-ci-cd'
project.description = 'Project with CI/CD Pipelines'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-ci-cd'
resource.description = 'Project with CI/CD Pipelines'
end
end
def name
......
......@@ -4,9 +4,11 @@ module QA
class SecretVariable < Factory::Base
attr_accessor :key, :value
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-secret-variables'
project.description = 'project for adding secret variable test'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-secret-variables'
resource.description = 'project for adding secret variable test'
end
end
def fabricate!
......
......@@ -10,9 +10,9 @@ module QA
attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key
product :title
product :fingerprint
attribute :private_key
attribute :title
attribute :fingerprint
def key
@key ||= Runtime::Key::RSA.new
......
......@@ -5,7 +5,6 @@ module QA
module Resource
class User < Factory::Base
attr_reader :unique_id
attr_writer :username, :password, :name, :email
def initialize
@unique_id = SecureRandom.hex(8)
......@@ -31,10 +30,10 @@ module QA
defined?(@username) && defined?(@password)
end
product :name
product :username
product :email
product :password
attribute :name
attribute :username
attribute :email
attribute :password
def fabricate!
# Don't try to log-out if we're not logged-in
......
......@@ -4,9 +4,11 @@ module QA
class Wiki < Factory::Base
attr_accessor :title, :content, :message
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-wikis'
project.description = 'project for adding wikis'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-for-wikis'
resource.description = 'project for adding wikis'
end
end
def fabricate!
......
......@@ -49,11 +49,12 @@ module QA
cluster.install_prometheus = true
cluster.install_runner = true
end
ingress_ip = kubernetes_cluster.ingress_ip
project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |p|
p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io")
p.enable_auto_devops_with_domain("#{ingress_ip}.nip.io")
end
project.visit!
......
......@@ -115,73 +115,20 @@ describe QA::Factory::Base do
end
end
describe '.dependency' do
let(:dependency) { spy('dependency') }
before do
stub_const('Some::MyDependency', dependency)
end
subject do
Class.new(described_class) do
dependency Some::MyDependency, as: :mydep do |factory|
factory.something!
end
end
end
it 'appends a new dependency and accessors' do
expect(subject.dependencies).to be_one
end
it 'defines dependency accessors' do
expect(subject.new).to respond_to :mydep, :mydep=
end
describe 'dependencies fabrication' do
let(:dependency) { double('dependency') }
let(:instance) { spy('instance') }
subject do
Class.new(described_class) do
dependency Some::MyDependency, as: :mydep
end
end
before do
stub_const('Some::MyDependency', dependency)
allow(subject).to receive(:new).and_return(instance)
allow(subject).to receive(:current_url).and_return(product_location)
allow(instance).to receive(:mydep).and_return(nil)
expect(QA::Factory::Product).to receive(:populate!)
end
it 'builds all dependencies first' do
expect(dependency).to receive(:fabricate!).once
subject.fabricate!
end
end
end
describe '.product' do
include_context 'fabrication context'
describe '.attribute' do
subject do
Class.new(described_class) do
def fabricate!
"any"
end
product :token
attribute :token
end
end
it 'appends new product attribute' do
expect(subject.attributes).to be_one
expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
expect(subject.attributes[0].name).to eq(:token)
expect(subject.attributes_names).to be_one
expect(subject.attributes_names[0]).to eq(:token)
end
end
end