gitlab-qa 10.4.1 → 11.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab/changelog_config.yml +13 -0
  3. data/.gitlab/merge_request_templates/Release.md +9 -36
  4. data/.rubocop_todo.yml +0 -12
  5. data/Dangerfile +1 -5
  6. data/Gemfile.lock +4 -4
  7. data/README.md +1 -2
  8. data/docs/running_against_remote_grid.md +39 -2
  9. data/gitlab-qa.gemspec +1 -1
  10. data/lib/gitlab/qa/component/gitaly_cluster.rb +0 -1
  11. data/lib/gitlab/qa/component/gitlab.rb +10 -9
  12. data/lib/gitlab/qa/component/selenoid.rb +8 -3
  13. data/lib/gitlab/qa/runtime/env.rb +21 -41
  14. data/lib/gitlab/qa/scenario/test/instance/airgapped.rb +0 -2
  15. data/lib/gitlab/qa/scenario/test/integration/gitaly_cluster.rb +0 -2
  16. data/lib/gitlab/qa/scenario/test/integration/mtls.rb +0 -1
  17. data/lib/gitlab/qa/scenario/test/integration/praefect.rb +0 -2
  18. data/lib/gitlab/qa/scenario/test/integration/registry_with_cdn.rb +2 -2
  19. data/lib/gitlab/qa/version.rb +1 -1
  20. data/lib/gitlab/qa.rb +0 -1
  21. data/support/data/admin_access_token_seed.rb +4 -1
  22. metadata +5 -39
  23. data/bin/slack +0 -14
  24. data/exe/gitlab-qa-report +0 -10
  25. data/lib/gitlab/qa/report/base_test_results.rb +0 -39
  26. data/lib/gitlab/qa/report/find_set_dri.rb +0 -43
  27. data/lib/gitlab/qa/report/generate_test_session.rb +0 -275
  28. data/lib/gitlab/qa/report/gitlab_issue_client.rb +0 -190
  29. data/lib/gitlab/qa/report/gitlab_issue_dry_client.rb +0 -28
  30. data/lib/gitlab/qa/report/j_unit_test_results.rb +0 -27
  31. data/lib/gitlab/qa/report/json_test_results.rb +0 -29
  32. data/lib/gitlab/qa/report/prepare_stage_reports.rb +0 -86
  33. data/lib/gitlab/qa/report/relate_failure_issue.rb +0 -374
  34. data/lib/gitlab/qa/report/report_as_issue.rb +0 -176
  35. data/lib/gitlab/qa/report/report_results.rb +0 -64
  36. data/lib/gitlab/qa/report/results_in_issues.rb +0 -126
  37. data/lib/gitlab/qa/report/results_in_testcases.rb +0 -111
  38. data/lib/gitlab/qa/report/results_reporter_shared.rb +0 -70
  39. data/lib/gitlab/qa/report/summary_table.rb +0 -43
  40. data/lib/gitlab/qa/report/test_result.rb +0 -184
  41. data/lib/gitlab/qa/report/update_screenshot_path.rb +0 -63
  42. data/lib/gitlab/qa/reporter.rb +0 -131
  43. data/lib/gitlab/qa/runtime/token_finder.rb +0 -44
  44. data/lib/gitlab/qa/slack/post_to_slack.rb +0 -30
  45. data/lib/gitlab/qa/system_logs/finders/json_log_finder.rb +0 -65
  46. data/lib/gitlab/qa/system_logs/finders/rails/api_log_finder.rb +0 -21
  47. data/lib/gitlab/qa/system_logs/finders/rails/application_log_finder.rb +0 -21
  48. data/lib/gitlab/qa/system_logs/finders/rails/exception_log_finder.rb +0 -21
  49. data/lib/gitlab/qa/system_logs/finders/rails/graphql_log_finder.rb +0 -21
  50. data/lib/gitlab/qa/system_logs/log_types/log.rb +0 -38
  51. data/lib/gitlab/qa/system_logs/log_types/rails/api_log.rb +0 -34
  52. data/lib/gitlab/qa/system_logs/log_types/rails/application_log.rb +0 -27
  53. data/lib/gitlab/qa/system_logs/log_types/rails/exception_log.rb +0 -23
  54. data/lib/gitlab/qa/system_logs/log_types/rails/graphql_log.rb +0 -30
  55. data/lib/gitlab/qa/system_logs/shared_fields.rb +0 -29
  56. data/lib/gitlab/qa/system_logs/system_logs_formatter.rb +0 -65
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'set'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- class ReportAsIssue
9
- MAX_TITLE_LENGTH = 255
10
-
11
- def initialize(token:, input_files:, project: nil, dry_run: false, **kwargs)
12
- @project = project
13
- @gitlab = (dry_run ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project)
14
- @files = Array(input_files)
15
- end
16
-
17
- def invoke!
18
- validate_input!
19
-
20
- run!
21
- end
22
-
23
- private
24
-
25
- attr_reader :gitlab, :files, :project, :issue_type
26
-
27
- def run!
28
- raise NotImplementedError
29
- end
30
-
31
- def new_issue_title(test)
32
- "#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
33
- end
34
-
35
- def new_issue_description(test)
36
- "### Full description\n\n#{search_safe(test.name)}\n\n### File path\n\n#{test.file}"
37
- end
38
-
39
- def new_issue_labels(test)
40
- []
41
- end
42
-
43
- def validate_input!
44
- assert_project!
45
- assert_input_files!(files)
46
- gitlab.assert_user_permission!
47
- end
48
-
49
- def assert_project!
50
- return if project
51
-
52
- abort "Please provide a valid project ID or path with the `-p/--project` option!"
53
- end
54
-
55
- def assert_input_files!(files)
56
- return if Dir.glob(files).any?
57
-
58
- abort "Please provide valid JUnit report files. No files were found matching `#{files.join(',')}`"
59
- end
60
-
61
- def test_results_per_file
62
- Dir.glob(files).each do |path|
63
- extension = File.extname(path)
64
-
65
- test_results =
66
- case extension
67
- when '.json'
68
- Report::JsonTestResults.new(path)
69
- when '.xml'
70
- Report::JUnitTestResults.new(path)
71
- else
72
- raise "Unknown extension #{extension}"
73
- end
74
-
75
- yield test_results
76
- end
77
- end
78
-
79
- def create_issue(test)
80
- issue = gitlab.create_issue(
81
- title: title_from_test(test),
82
- description: new_issue_description(test),
83
- labels: new_issue_labels(test).to_a,
84
- issue_type: issue_type
85
- )
86
-
87
- new_link = issue_type == 'test_case' ? issue.web_url.sub('/issues/', '/quality/test_cases/') : issue.web_url
88
-
89
- puts "Created new #{issue_type}: #{new_link}"
90
-
91
- issue
92
- end
93
-
94
- def issue_labels(issue)
95
- issue&.labels&.to_set || Set.new
96
- end
97
-
98
- def update_labels(issue, test, new_labels = Set.new)
99
- labels = up_to_date_labels(test: test, issue: issue, new_labels: new_labels)
100
-
101
- return if issue_labels(issue) == labels
102
-
103
- gitlab.edit_issue(iid: issue.iid, options: { labels: labels.to_a })
104
- end
105
-
106
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
107
- labels = issue_labels(issue)
108
- labels |= new_labels
109
- ee_test?(test) ? labels << 'Enterprise Edition' : labels.delete('Enterprise Edition')
110
-
111
- if test.quarantine?
112
- labels << 'quarantine'
113
- labels << "quarantine::#{test.quarantine_type}"
114
- else
115
- labels.delete_if { |label| label.include?('quarantine') }
116
- end
117
-
118
- labels
119
- end
120
-
121
- def pipeline_name_label
122
- case pipeline
123
- when 'production'
124
- 'found:gitlab.com'
125
- when 'canary', 'staging'
126
- "found:#{pipeline}.gitlab.com"
127
- when 'staging-canary'
128
- "found:canary.staging.gitlab.com"
129
- when 'preprod'
130
- 'found:pre.gitlab.com'
131
- when 'nightly', QA::Runtime::Env.default_branch, 'staging-ref', 'release'
132
- "found:#{pipeline}"
133
- else
134
- raise "No `found:*` label for the `#{pipeline}` pipeline!"
135
- end
136
- end
137
-
138
- def ee_test?(test)
139
- test.file =~ %r{features/ee/(api|browser_ui)}
140
- end
141
-
142
- def partial_file_path(path)
143
- path.match(/((ee|api|browser_ui).*)/i)[1]
144
- end
145
-
146
- def title_from_test(test)
147
- title = new_issue_title(test)
148
-
149
- return title unless title.length > MAX_TITLE_LENGTH
150
-
151
- "#{title[0...MAX_TITLE_LENGTH - 3]}..."
152
- end
153
-
154
- def search_safe(value)
155
- value.delete('"')
156
- end
157
-
158
- def pipeline
159
- # Gets the name of the pipeline the test was run in, to be used as the key of a scoped label
160
- #
161
- # Tests can be run in several pipelines:
162
- # gitlab, nightly, staging, canary, production, preprod, MRs, and the default branch (master/main)
163
- #
164
- # Some of those run in their own project, so CI_PROJECT_NAME is the name we need. Those are:
165
- # nightly, staging, canary, production, and preprod
166
- #
167
- # MR, master/main, and gitlab tests run in gitlab-qa, but we only want to report tests run on
168
- # master/main because the other pipelines will be monitored by the author of the MR that triggered them.
169
- # So we assume that we're reporting a master/main pipeline if the project name is 'gitlab'.
170
-
171
- @pipeline ||= Runtime::Env.pipeline_from_project_name
172
- end
173
- end
174
- end
175
- end
176
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gitlab
4
- module QA
5
- module Report
6
- # Uses the API to create or update GitLab test cases and issues with the results of tests from RSpec report files.
7
- class ReportResults < ReportAsIssue
8
- attr_accessor :testcase_project_reporter, :results_issue_project_reporter, :files, :test_case_project, :results_issue_project, :gitlab
9
-
10
- def initialize(token:, input_files:, test_case_project: nil, results_issue_project: nil, dry_run: false, **kwargs)
11
- @testcase_project_reporter = Gitlab::QA::Report::ResultsInTestCases.new(token: token, input_files: input_files, project: test_case_project, dry_run: dry_run, **kwargs)
12
- @results_issue_project_reporter = Gitlab::QA::Report::ResultsInIssues.new(token: token, input_files: input_files, project: results_issue_project, dry_run: dry_run, **kwargs)
13
- @test_case_project = test_case_project
14
- @results_issue_project = results_issue_project
15
- @files = Array(input_files)
16
- @gitlab = testcase_project_reporter.gitlab
17
- end
18
-
19
- def assert_project!
20
- return if test_case_project && results_issue_project
21
-
22
- abort "Please provide valid project IDs or paths with the `--results-issue-project` and `--test-case-project` options!"
23
- end
24
-
25
- # rubocop:disable Metrics/AbcSize
26
- def run!
27
- puts "Reporting test results in `#{files.join(',')}` as test cases in project `#{test_case_project}`"\
28
- " and issues in project `#{results_issue_project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
29
-
30
- test_results_per_file do |test_results|
31
- puts "Reporting tests in #{test_results.path}"
32
-
33
- test_results.each do |test|
34
- next if test.file.include?('/features/sanity/') || test.skipped
35
-
36
- puts "Reporting test: #{test.file} | #{test.name}\n"
37
-
38
- report_test(test)
39
- end
40
-
41
- test_results.write
42
- end
43
- end
44
- # rubocop:enable Metrics/AbcSize
45
-
46
- private
47
-
48
- def report_test(test)
49
- testcase = testcase_project_reporter.find_or_create_testcase(test)
50
- # The API returns the test case with an issue URL since it is technically a type of issue.
51
- # This updates the URL to a valid test case link.
52
- test.testcase = testcase.web_url.sub('/issues/', '/quality/test_cases/')
53
-
54
- issue, is_new = results_issue_project_reporter.get_related_issue(testcase, test)
55
-
56
- testcase_project_reporter.add_issue_link_to_testcase(testcase, issue, test) if is_new
57
-
58
- testcase_project_reporter.update_testcase(testcase, test)
59
- results_issue_project_reporter.update_issue(issue, test)
60
- end
61
- end
62
- end
63
- end
64
- end
@@ -1,126 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gitlab
4
- module QA
5
- module Report
6
- # Uses the API to create or update GitLab test result issues with the results of tests from RSpec report files.
7
- class ResultsInIssues < ReportAsIssue
8
- include ResultsReporterShared
9
-
10
- def initialize(**kwargs)
11
- super
12
- @issue_type = 'issue'
13
- end
14
-
15
- def get_related_issue(testcase, test)
16
- issue = find_linked_results_issue_by_iid(testcase, test)
17
- is_new = false
18
-
19
- if issue
20
- issue = update_issue_title(issue, test) if issue_title_needs_updating?(issue, test)
21
- else
22
- puts "No valid issue link found"
23
- issue = find_or_create_results_issue(test)
24
- is_new = true
25
- end
26
-
27
- [issue, is_new]
28
- end
29
-
30
- def update_issue(issue, test)
31
- new_labels = issue_labels(issue)
32
- new_labels |= ['Testcase Linked']
33
-
34
- labels_updated = update_labels(issue, test, new_labels)
35
- note_posted = note_status(issue, test)
36
-
37
- if labels_updated || note_posted
38
- puts "Issue updated: #{issue.web_url}"
39
- else
40
- puts "Test passed, no results issue update needed."
41
- end
42
- end
43
-
44
- private
45
-
46
- def find_linked_results_issue_by_iid(testcase, test)
47
- iid = issue_iid_from_testcase(testcase)
48
-
49
- return unless iid
50
-
51
- find_issue_by_iid(iid)
52
- end
53
-
54
- def find_or_create_results_issue(test)
55
- find_issue(test) || create_issue(test)
56
- end
57
-
58
- def issue_iid_from_testcase(testcase)
59
- results = testcase.description.partition(TEST_CASE_RESULTS_SECTION_TEMPLATE).last if testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE)
60
-
61
- return puts "No issue link found" unless results
62
-
63
- issue_iid = results.split('/').last
64
-
65
- issue_iid&.to_i
66
- end
67
-
68
- def note_status(issue, test)
69
- return false if test.skipped
70
- return false if test.failures.empty?
71
-
72
- note = note_content(test)
73
-
74
- gitlab.find_issue_discussions(iid: issue.iid).each do |discussion|
75
- return gitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid, discussion_id: discussion.id, body: failure_summary) if new_note_matches_discussion?(note, discussion)
76
- end
77
-
78
- gitlab.create_issue_note(iid: issue.iid, note: note)
79
-
80
- true
81
- end
82
-
83
- def note_content(test)
84
- errors = test.failures.each_with_object([]) do |failure, text|
85
- text << <<~TEXT
86
- Error:
87
- ```
88
- #{failure['message']}
89
- ```
90
-
91
- Stacktrace:
92
- ```
93
- #{failure['stacktrace']}
94
- ```
95
- TEXT
96
- end.join("\n\n")
97
-
98
- "#{failure_summary}\n\n#{errors}"
99
- end
100
-
101
- def failure_summary
102
- summary = [":x: ~\"#{pipeline}::failed\""]
103
- summary << "in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}"
104
- summary.join(' ')
105
- end
106
-
107
- def new_note_matches_discussion?(note, discussion)
108
- note_error = error_and_stack_trace(note)
109
- discussion_error = error_and_stack_trace(discussion.notes.first['body'])
110
-
111
- return false if note_error.empty? || discussion_error.empty?
112
-
113
- note_error == discussion_error
114
- end
115
-
116
- def error_and_stack_trace(text)
117
- text.strip[/Error:(.*)/m, 1].to_s
118
- end
119
-
120
- def updated_description(issue, test)
121
- new_issue_description(test)
122
- end
123
- end
124
- end
125
- end
126
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'erb'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- # Uses the API to create or update GitLab test cases with the results of tests from RSpec report files.
9
- class ResultsInTestCases < ReportAsIssue
10
- attr_reader :issue_type, :gitlab
11
-
12
- include ResultsReporterShared
13
-
14
- def initialize(**kwargs)
15
- super
16
- @issue_type = 'test_case'
17
- end
18
-
19
- def find_or_create_testcase(test)
20
- find_testcase(test) || create_issue(test)
21
- end
22
-
23
- def add_issue_link_to_testcase(testcase, issue, test)
24
- results_section = testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE) ? '' : TEST_CASE_RESULTS_SECTION_TEMPLATE
25
-
26
- gitlab.edit_issue(iid: testcase.iid, options: { description: (testcase.description + results_section + "\n\n#{issue.web_url}") })
27
- # We are using test.testcase for the url here instead of testcase.web_url since it has the updated test case path
28
- puts "Added results issue #{issue.web_url} link to test case #{test.testcase}"
29
- end
30
-
31
- def update_testcase(testcase, test)
32
- puts "Test case labels updated." if update_labels(testcase, test)
33
- puts "Test case quarantine section updated." if update_quarantine_link(testcase, test)
34
- end
35
-
36
- private
37
-
38
- def find_testcase(test)
39
- testcase = find_testcase_by_iid(test)
40
-
41
- if testcase
42
- testcase = update_issue_title(testcase, test) if issue_title_needs_updating?(testcase, test)
43
- else
44
- testcase = find_issue(test)
45
- end
46
-
47
- testcase
48
- end
49
-
50
- def find_testcase_by_iid(test)
51
- iid = testcase_iid_from_url(test.testcase)
52
-
53
- return unless iid
54
-
55
- find_issue_by_iid(iid)
56
- end
57
-
58
- def testcase_iid_from_url(url)
59
- return warn(%(\nPlease update #{url} to test case url")) if url&.include?('/-/issues/')
60
-
61
- url && url.split('/').last.to_i
62
- end
63
-
64
- def new_issue_description(test)
65
- quarantine_section = test.quarantine? && test.quarantine_issue ? "\n\n### Quarantine issue\n\n#{test.quarantine_issue}" : ''
66
-
67
- "#{super}#{quarantine_section}\n\n#{execution_graph_section(test)}"
68
- end
69
-
70
- def execution_graph_section(test)
71
- formatted_title = ERB::Util.url_encode(test.name)
72
-
73
- <<~MKDOWN.strip
74
- ### Executions
75
-
76
- All Environments:
77
- <img src="https://dashboards.quality.gitlab.net/render/d-solo/cW0UMgv7k/spec-health?orgId=1&var-run_type=All&var-name=#{formatted_title}&panelId=4&width=1000&height=500" />
78
- MKDOWN
79
- end
80
-
81
- def updated_description(testcase, test)
82
- historical_results_section = testcase.description.match(/### DO NOT EDIT BELOW THIS LINE[\s\S]+/)
83
-
84
- "#{new_issue_description(test)}\n\n#{historical_results_section}"
85
- end
86
-
87
- def issue_title_needs_updating?(testcase, test)
88
- super || !testcase.description.include?(execution_graph_section(test)) && !%w[canary production preprod release].include?(pipeline)
89
- end
90
-
91
- def quarantine_link_needs_updating?(testcase, test)
92
- if test.quarantine? && test.quarantine_issue
93
- return false if testcase.description.include?(test.quarantine_issue)
94
- else
95
- return false unless testcase.description.include?('Quarantine issue')
96
- end
97
-
98
- true
99
- end
100
-
101
- def update_quarantine_link(testcase, test)
102
- return unless quarantine_link_needs_updating?(testcase, test)
103
-
104
- new_description = updated_description(testcase, test)
105
-
106
- gitlab.edit_issue(iid: testcase.iid, options: { description: new_description })
107
- end
108
- end
109
- end
110
- end
111
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/core_ext/enumerable'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- module ResultsReporterShared
9
- TEST_CASE_RESULTS_SECTION_TEMPLATE = "\n\n### DO NOT EDIT BELOW THIS LINE\n\nActive and historical test results:"
10
-
11
- def find_issue(test)
12
- issues = search_for_issues(test)
13
-
14
- warn(%(Too many #{issue_type}s found with the file path "#{test.file}" and name "#{test.name}")) if issues.many?
15
- puts "Found existing #{issue_type}: #{issues.first.web_url}" unless issues.empty?
16
-
17
- issues.first
18
- end
19
-
20
- def find_issue_by_iid(iid)
21
- issues = gitlab.find_issues(iid: iid) do |issue|
22
- issue.state == 'opened' && issue.issue_type == issue_type
23
- end
24
-
25
- warn(%(#{issue_type} iid "#{iid}" not valid)) if issues.empty?
26
-
27
- issues.first
28
- end
29
-
30
- def issue_title_needs_updating?(issue, test)
31
- issue.title.strip != title_from_test(test) && !%w[canary production preprod release].include?(pipeline)
32
- end
33
-
34
- def new_issue_labels(test)
35
- %w[Quality status::automated]
36
- end
37
-
38
- def search_term(test)
39
- %("#{partial_file_path(test.file)}" "#{search_safe(test.name)}")
40
- end
41
-
42
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
43
- labels = super
44
- labels |= new_issue_labels(test).to_set
45
- labels.delete_if { |label| label.start_with?("#{pipeline}::") }
46
- labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
47
- end
48
-
49
- def update_issue_title(issue, test)
50
- old_title = issue.title.strip
51
- new_title = title_from_test(test)
52
-
53
- warn(%(#{issue_type} title needs to be updated from '#{old_title}' to '#{new_title}'))
54
-
55
- new_description = updated_description(issue, test)
56
-
57
- gitlab.edit_issue(iid: issue.iid, options: { title: new_title, description: new_description })
58
- end
59
-
60
- private
61
-
62
- def search_for_issues(test)
63
- gitlab.find_issues(options: { search: search_term(test) }) do |issue|
64
- issue.state == 'opened' && issue.issue_type == issue_type && issue.title.strip == title_from_test(test)
65
- end
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nokogiri'
4
- require 'table_print'
5
-
6
- module Gitlab
7
- module QA
8
- module Report
9
- class SummaryTable
10
- def self.create(input_files:)
11
- "```\n#{TablePrint::Printer.table_print(collect_results(input_files))}```\n"
12
- end
13
-
14
- # rubocop:disable Metrics/AbcSize
15
- def self.collect_results(input_files)
16
- stage_wise_results = []
17
-
18
- Dir.glob(input_files).each do |report_file|
19
- stage_hash = {}
20
- stage_hash["Dev Stage"] = File.basename(report_file, ".*").capitalize
21
-
22
- report_stats = Nokogiri::XML(File.open(report_file)).children[0].attributes
23
-
24
- stage_hash["Total"] = report_stats["tests"].value
25
- stage_hash["Failures"] = report_stats["failures"].value
26
- stage_hash["Errors"] = report_stats["errors"].value
27
- stage_hash["Skipped"] = report_stats["skipped"].value
28
- stage_hash["Result"] = result_emoji(report_stats)
29
-
30
- stage_wise_results << stage_hash
31
- end
32
-
33
- stage_wise_results
34
- end
35
- # rubocop:enable Metrics/AbcSize
36
-
37
- def self.result_emoji(report_stats)
38
- report_stats["failures"].value.to_i.positive? || report_stats["errors"].value.to_i.positive? ? "❌" : "✅"
39
- end
40
- end
41
- end
42
- end
43
- end