Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 15:26:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 15:26:25 +0300
commita09983ae35713f5a2bbb100981116d31ce99826e (patch)
tree2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/support/shared_examples/services
parent18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff)
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/support/shared_examples/services')
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/services/common_system_notes_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb193
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb10
11 files changed, 409 insertions, 48 deletions
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
new file mode 100644
index 00000000000..a1354a8099b
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'creates an alert management alert' do
+ it { is_expected.to be_success }
+
+ it 'creates AlertManagement::Alert' do
+ expect { subject }.to change(AlertManagement::Alert, :count).by(1)
+ end
+
+ it 'executes the alert service hooks' do
+ slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true)
+
+ subject
+
+ expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash))
+ end
+end
+
+RSpec.shared_examples 'does not an create alert management alert' do
+ it 'does not create alert' do
+ expect { subject }.not_to change(AlertManagement::Alert, :count)
+ end
+end
+
+RSpec.shared_examples 'adds an alert management alert event' do
+ it { is_expected.to be_success }
+
+ it 'does not create an alert' do
+ expect { subject }.not_to change(AlertManagement::Alert, :count)
+ end
+
+ it 'increases alert events count' do
+ expect { subject }.to change { alert.reload.events }.by(1)
+ end
+
+ it 'does not executes the alert service hooks' do
+ expect(alert).not_to receive(:execute_services)
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb
new file mode 100644
index 00000000000..cbe20928f98
--- /dev/null
+++ b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'parse cluster applications artifact' do |release_name|
+ let(:application_class) { Clusters::Cluster::APPLICATIONS[release_name] }
+ let(:cluster_application) { cluster.public_send("application_#{release_name}") }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'release is missing' do
+ let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_missing.json.gz" }
+
+ context 'application does not exist' do
+ it 'does not create or destroy an application' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.not_to change(application_class, :count)
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster)
+ end
+
+ it 'marks the application as uninstalled' do
+ described_class.new(job, user).execute(artifact)
+
+ cluster_application.reload
+ expect(cluster_application).to be_uninstalled
+ end
+ end
+ end
+
+ context 'release is deployed' do
+ let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_deployed.json.gz" }
+
+ context 'application does not exist' do
+ it 'creates an application and marks it as installed' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to change(application_class, :count)
+
+ expect(cluster_application).to be_persisted
+ expect(cluster_application).to be_installed
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create("clusters_applications_#{release_name}".to_sym, :errored, cluster: cluster)
+ end
+
+ it 'marks the application as installed' do
+ described_class.new(job, user).execute(artifact)
+
+ expect(cluster_application).to be_installed
+ end
+ end
+ end
+
+ context 'release is failed' do
+ let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_failed.json.gz" }
+
+ context 'application does not exist' do
+ it 'creates an application and marks it as errored' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to change(application_class, :count)
+
+ expect(cluster_application).to be_persisted
+ expect(cluster_application).to be_errored
+ expect(cluster_application.status_reason).to eq('Helm release failed to install')
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster)
+ end
+
+ it 'marks the application as errored' do
+ described_class.new(job, user).execute(artifact)
+
+ expect(cluster_application).to be_errored
+ expect(cluster_application.status_reason).to eq('Helm release failed to install')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
index 4ce3e32d774..20856b05de6 100644
--- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
+++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
@@ -17,10 +17,10 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text|
end
end
-RSpec.shared_examples 'WIP notes creation' do |wip_action|
+RSpec.shared_examples 'draft notes creation' do |wip_action|
subject { described_class.new(project, user).execute(issuable, old_labels: []) }
- it 'creates WIP toggle and title change notes' do
+ it 'creates Draft toggle and title change notes' do
expect { subject }.to change { Note.count }.from(0).to(2)
expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
diff --git a/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb b/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb
index c5e56ed3539..8fd76f7cb1f 100644
--- a/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples 'responds with error' do |message|
+RSpec.shared_examples 'responds with error' do |message|
it 'returns error' do
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_error
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
index 5dd1badbefc..c8fabfe30b9 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -29,14 +29,43 @@ RSpec.shared_examples 'valid dashboard service response' do
end
RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do
- it do
- expect(YAML).to receive(:safe_load).once.and_call_original
+ specify do
+ expect_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader|
+ expect(loader).to receive(:load_raw!).once.and_call_original
+ end
described_class.new(*service_params).get_dashboard
described_class.new(*service_params).get_dashboard
end
end
+# This spec is applicable for predefined/out-of-the-box dashboard services.
+RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do
+ specify do
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:dashboard_version).and_return('1', '2')
+ end
+
+ expect(File).to receive(:read).twice.and_call_original
+
+ service = described_class.new(*service_params)
+
+ service.get_dashboard
+ service.get_dashboard
+ end
+end
+
+# This spec is applicable for predefined/out-of-the-box dashboard services.
+# This shared_example requires the following variables to be defined:
+# dashboard_path: Relative path to the dashboard, ex: 'config/prometheus/common_metrics.yml'
+# dashboard_version: The version string used in the cache_key.
+RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do
+ specify do
+ dashboard = File.read(Rails.root.join(dashboard_path))
+ expect(Digest::SHA256.hexdigest(dashboard)).to eq(dashboard_version)
+ end
+end
+
RSpec.shared_examples 'valid embedded dashboard service response' do
let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
@@ -128,3 +157,50 @@ RSpec.shared_examples 'updates gitlab_metrics_dashboard_processing_time_ms metri
expect(metric.get(labels)).to be > 0
end
end
+
+RSpec.shared_examples '#raw_dashboard raises error if dashboard loading fails' do
+ context 'when yaml is too large' do
+ before do
+ allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader|
+ allow(loader).to receive(:load_raw!)
+ .and_raise(Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
+ end
+ end
+
+ it 'raises error' do
+ expect { subject.raw_dashboard }.to raise_error(
+ Gitlab::Metrics::Dashboard::Errors::LayoutError,
+ 'The parsed YAML is too big'
+ )
+ end
+ end
+
+ context 'when yaml loader returns error' do
+ before do
+ allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader|
+ allow(loader).to receive(:load_raw!)
+ .and_raise(Gitlab::Config::Loader::FormatError, 'Invalid configuration format')
+ end
+ end
+
+ it 'raises error' do
+ expect { subject.raw_dashboard }.to raise_error(
+ Gitlab::Metrics::Dashboard::Errors::LayoutError,
+ 'Invalid yaml'
+ )
+ end
+ end
+
+ context 'when yaml is not a hash' do
+ before do
+ allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader|
+ allow(loader).to receive(:load_raw!)
+ .and_raise(Gitlab::Config::Loader::Yaml::NotHashError, 'Invalid configuration format')
+ end
+ end
+
+ it 'returns nil' do
+ expect(subject.raw_dashboard).to eq({})
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
new file mode 100644
index 00000000000..45a4c2bb151
--- /dev/null
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'assigns build to package' do
+ context 'with build info' do
+ let(:job) { create(:ci_build, user: user) }
+ let(:params) { super().merge(build: job) }
+
+ it 'assigns the pipeline to the package' do
+ package = subject
+
+ expect(package.build_info).to be_present
+ expect(package.build_info.pipeline).to eq job.pipeline
+ end
+ end
+end
+
+RSpec.shared_examples 'returns packages' do |container_type, user_type|
+ context "for #{user_type}" do
+ before do
+ send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type
+ end
+
+ it 'returns success response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ it 'returns a valid response schema' do
+ subject
+
+ expect(response).to match_response_schema(package_schema)
+ end
+
+ it 'returns two packages' do
+ subject
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id)
+ end
+ end
+end
+
+RSpec.shared_examples 'returns packages with subgroups' do |container_type, user_type|
+ context "with subgroups for #{user_type}" do
+ before do
+ send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type
+ end
+
+ it 'returns success response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ it 'returns a valid response schema' do
+ subject
+
+ expect(response).to match_response_schema(package_schema)
+ end
+
+ it 'returns three packages' do
+ subject
+
+ expect(json_response.length).to eq(3)
+ expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id)
+ end
+ end
+end
+
+RSpec.shared_examples 'package sorting' do |order_by|
+ subject { get api(url), params: { sort: sort, order_by: order_by } }
+
+ context "sorting by #{order_by}" do
+ context 'ascending order' do
+ let(:sort) { 'asc' }
+
+ it 'returns the sorted packages' do
+ subject
+
+ expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id))
+ end
+ end
+
+ context 'descending order' do
+ let(:sort) { 'desc' }
+
+ it 'returns the sorted packages' do
+ subject
+
+ expect(json_response.map { |package| package['id'] }).to eq(packages.reverse.map(&:id))
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects packages access' do |container_type, user_type, status|
+ context "for #{user_type}" do
+ before do
+ send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type
+ end
+
+ it_behaves_like 'returning response status', status
+ end
+end
+
+RSpec.shared_examples 'returns paginated packages' do
+ let(:per_page) { 2 }
+
+ context 'when viewing the first page' do
+ let(:page) { 1 }
+
+ it 'returns first 2 packages' do
+ get api(url, user), params: { page: page, per_page: per_page }
+
+ expect_paginated_array_response([package1.id, package2.id])
+ end
+ end
+
+ context 'when viewing the second page' do
+ let(:page) { 2 }
+
+ it 'returns first 2 packages' do
+ get api(url, user), params: { page: page, per_page: per_page }
+
+ expect_paginated_array_response([package3.id, package4.id])
+ end
+ end
+end
+
+RSpec.shared_examples 'background upload schedules a file migration' do
+ context 'background upload enabled' do
+ before do
+ stub_package_file_object_storage(background_upload: true)
+ end
+
+ it 'schedules migration of file to object storage' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric))
+
+ subject
+ end
+ end
+end
+
+RSpec.shared_context 'package filter context' do
+ def package_filter_url(filter, param)
+ "/projects/#{project.id}/packages?package_#{filter}=#{param}"
+ end
+
+ def group_filter_url(filter, param)
+ "/groups/#{group.id}/packages?package_#{filter}=#{param}"
+ end
+end
+
+RSpec.shared_examples 'filters on each package_type' do |is_project: false|
+ include_context 'package filter context'
+
+ let_it_be(:package1) { create(:conan_package, project: project) }
+ let_it_be(:package2) { create(:maven_package, project: project) }
+ let_it_be(:package3) { create(:npm_package, project: project) }
+ let_it_be(:package4) { create(:nuget_package, project: project) }
+ let_it_be(:package5) { create(:pypi_package, project: project) }
+ let_it_be(:package6) { create(:composer_package, project: project) }
+
+ Packages::Package.package_types.keys.each do |package_type|
+ context "for package type #{package_type}" do
+ let(:url) { is_project ? package_filter_url(:type, package_type) : group_filter_url(:type, package_type) }
+
+ subject { get api(url, user) }
+
+ it "returns #{package_type} packages" do
+ subject
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.map { |package| package['package_type'] }).to contain_exactly(package_type)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'package workhorse uploads' do
+ context 'without a workhorse header' do
+ let(:workhorse_token) { JWT.encode({ 'iss' => 'invalid header' }, Gitlab::Workhorse.secret, 'HS256') }
+
+ it_behaves_like 'returning response status', :forbidden
+
+ it 'logs an error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ subject
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index 0e6ecf49cd0..2ddbdebdb97 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -25,19 +25,18 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid)
- allow(project_repository_double).to receive(:create_repository)
- .and_return(true)
allow(project_repository_double).to receive(:replicate)
.with(project.repository.raw)
allow(project_repository_double).to receive(:checksum)
.and_return(project_repository_checksum)
- allow(repository_double).to receive(:create_repository)
- .and_return(true)
allow(repository_double).to receive(:replicate)
.with(repository.raw)
allow(repository_double).to receive(:checksum)
.and_return(repository_checksum)
+
+ expect(GitlabShellWorker).to receive(:perform_async).with(:mv_repository, 'default', anything, anything)
+ .twice.and_call_original
end
it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do
@@ -48,6 +47,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
old_repository_path = repository.full_path
result = subject.execute
+ project.reload
expect(result).to be_success
expect(project).not_to be_repository_read_only
@@ -101,15 +101,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'unmarks the repository as read-only without updating the repository storage' do
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid)
- allow(project_repository_double).to receive(:create_repository)
- .and_return(true)
allow(project_repository_double).to receive(:replicate)
.with(project.repository.raw)
allow(project_repository_double).to receive(:checksum)
.and_return(project_repository_checksum)
- allow(repository_double).to receive(:create_repository)
- .and_return(true)
allow(repository_double).to receive(:replicate)
.with(repository.raw)
.and_raise(Gitlab::Git::CommandError)
@@ -128,15 +124,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'unmarks the repository as read-only without updating the repository storage' do
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid)
- allow(project_repository_double).to receive(:create_repository)
- .and_return(true)
allow(project_repository_double).to receive(:replicate)
.with(project.repository.raw)
allow(project_repository_double).to receive(:checksum)
.and_return(project_repository_checksum)
- allow(repository_double).to receive(:create_repository)
- .and_return(true)
allow(repository_double).to receive(:replicate)
.with(repository.raw)
allow(repository_double).to receive(:checksum)
diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
index c5f84e205cf..ef41c2fcc13 100644
--- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples 'a milestone events creator' do
+RSpec.shared_examples 'a milestone events creator' do
let_it_be(:user) { create(:user) }
let(:created_at_time) { Time.utc(2019, 12, 30) }
diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
index efcb83a34af..ebe78c299a5 100644
--- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
include_examples 'correct event created'
end
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute }.not_to change(Event, :count)
- end
- end
-
context 'when the options are bad' do
let(:page_title) { '' }
diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
index 1231c012c31..db1b50fdf3c 100644
--- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
end
-
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute(page) }.not_to change(Event, :count)
- end
- end
end
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
index 77354fec069..0191a6dfbc9 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
include_examples 'adds activity event'
end
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute(page) }.not_to change(Event, :count)
- end
- end
-
context 'when the options are bad' do
let(:page_title) { '' }