gitlab_quality-test_tooling 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -4
- data/Gemfile.lock +1 -1
- data/Guardfile +0 -22
- data/README.md +150 -9
- data/exe/generate-test-session +50 -0
- data/exe/post-to-slack +58 -0
- data/exe/prepare-stage-reports +38 -0
- data/exe/relate-failure-issue +59 -0
- data/exe/report-results +56 -0
- data/exe/update-screenshot-paths +38 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +194 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb +26 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +51 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +75 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +49 -0
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +275 -0
- data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +78 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +377 -0
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +134 -0
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +130 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_testcases.rb +113 -0
- data/lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb +81 -0
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +113 -0
- data/lib/gitlab_quality/test_tooling/runtime/logger.rb +92 -0
- data/lib/gitlab_quality/test_tooling/runtime/token_finder.rb +44 -0
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +36 -0
- data/lib/gitlab_quality/test_tooling/summary_table.rb +41 -0
- data/lib/gitlab_quality/test_tooling/support/http_request.rb +34 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb +65 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/log.rb +38 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/api_log.rb +34 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/application_log.rb +27 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/exception_log.rb +23 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb +30 -0
- data/lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb +29 -0
- data/lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb +65 -0
- data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +39 -0
- data/lib/gitlab_quality/test_tooling/test_results/builder.rb +35 -0
- data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +27 -0
- data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +29 -0
- data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +184 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +11 -2
- metadata +51 -3
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
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 Concerns::ResultsReporter
|
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
|
+
if new_note_matches_discussion?(
|
76
|
+
note, discussion)
|
77
|
+
return gitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid, discussion_id: discussion.id,
|
78
|
+
body: failure_summary)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
gitlab.create_issue_note(iid: issue.iid, note: note)
|
83
|
+
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
def note_content(test)
|
88
|
+
errors = test.failures.each_with_object([]) do |failure, text|
|
89
|
+
text << <<~TEXT
|
90
|
+
Error:
|
91
|
+
```
|
92
|
+
#{failure['message']}
|
93
|
+
```
|
94
|
+
|
95
|
+
Stacktrace:
|
96
|
+
```
|
97
|
+
#{failure['stacktrace']}
|
98
|
+
```
|
99
|
+
TEXT
|
100
|
+
end.join("\n\n")
|
101
|
+
|
102
|
+
"#{failure_summary}\n\n#{errors}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def failure_summary
|
106
|
+
summary = [":x: ~\"#{pipeline}::failed\""]
|
107
|
+
summary << "in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}"
|
108
|
+
summary.join(' ')
|
109
|
+
end
|
110
|
+
|
111
|
+
def new_note_matches_discussion?(note, discussion)
|
112
|
+
note_error = error_and_stack_trace(note)
|
113
|
+
discussion_error = error_and_stack_trace(discussion.notes.first['body'])
|
114
|
+
|
115
|
+
return false if note_error.empty? || discussion_error.empty?
|
116
|
+
|
117
|
+
note_error == discussion_error
|
118
|
+
end
|
119
|
+
|
120
|
+
def error_and_stack_trace(text)
|
121
|
+
text.strip[/Error:(.*)/m, 1].to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
def updated_description(_issue, test)
|
125
|
+
new_issue_description(test)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
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
|
+
include Concerns::ResultsReporter
|
11
|
+
|
12
|
+
attr_reader :issue_type, :gitlab
|
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,
|
27
|
+
options: { description: (testcase.description + results_section + "\n\n#{issue.web_url}") })
|
28
|
+
# We are using test.testcase for the url here instead of testcase.web_url since it has the updated test case path
|
29
|
+
puts "Added results issue #{issue.web_url} link to test case #{test.testcase}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_testcase(testcase, test)
|
33
|
+
puts "Test case labels updated." if update_labels(testcase, test)
|
34
|
+
puts "Test case quarantine section updated." if update_quarantine_link(testcase, test)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def find_testcase(test)
|
40
|
+
testcase = find_testcase_by_iid(test)
|
41
|
+
|
42
|
+
if testcase
|
43
|
+
testcase = update_issue_title(testcase, test) if issue_title_needs_updating?(testcase, test)
|
44
|
+
else
|
45
|
+
testcase = find_issue(test)
|
46
|
+
end
|
47
|
+
|
48
|
+
testcase
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_testcase_by_iid(test)
|
52
|
+
iid = testcase_iid_from_url(test.testcase)
|
53
|
+
|
54
|
+
return unless iid
|
55
|
+
|
56
|
+
find_issue_by_iid(iid)
|
57
|
+
end
|
58
|
+
|
59
|
+
def testcase_iid_from_url(url)
|
60
|
+
return warn(%(\nPlease update #{url} to test case url")) if url&.include?('/-/issues/')
|
61
|
+
|
62
|
+
url && url.split('/').last.to_i
|
63
|
+
end
|
64
|
+
|
65
|
+
def new_issue_description(test)
|
66
|
+
quarantine_section = test.quarantine? && test.quarantine_issue ? "\n\n### Quarantine issue\n\n#{test.quarantine_issue}" : ''
|
67
|
+
|
68
|
+
"#{super}#{quarantine_section}\n\n#{execution_graph_section(test)}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def execution_graph_section(test)
|
72
|
+
formatted_title = ERB::Util.url_encode(test.name)
|
73
|
+
|
74
|
+
<<~MKDOWN.strip
|
75
|
+
### Executions
|
76
|
+
|
77
|
+
All Environments:
|
78
|
+
<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" />
|
79
|
+
MKDOWN
|
80
|
+
end
|
81
|
+
|
82
|
+
def updated_description(testcase, test)
|
83
|
+
historical_results_section = testcase.description.match(/### DO NOT EDIT BELOW THIS LINE[\s\S]+/)
|
84
|
+
|
85
|
+
"#{new_issue_description(test)}\n\n#{historical_results_section}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def issue_title_needs_updating?(testcase, test)
|
89
|
+
super || (!testcase.description.include?(execution_graph_section(test)) && !%w[canary production preprod
|
90
|
+
release].include?(pipeline))
|
91
|
+
end
|
92
|
+
|
93
|
+
def quarantine_link_needs_updating?(testcase, test)
|
94
|
+
if test.quarantine? && test.quarantine_issue
|
95
|
+
return false if testcase.description.include?(test.quarantine_issue)
|
96
|
+
else
|
97
|
+
return false unless testcase.description.include?('Quarantine issue')
|
98
|
+
end
|
99
|
+
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def update_quarantine_link(testcase, test)
|
104
|
+
return unless quarantine_link_needs_updating?(testcase, test)
|
105
|
+
|
106
|
+
new_description = updated_description(testcase, test)
|
107
|
+
|
108
|
+
gitlab.edit_issue(iid: testcase.iid, options: { description: new_description })
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'json'
|
5
|
+
require 'active_support/core_ext/object/blank'
|
6
|
+
|
7
|
+
module GitlabQuality
|
8
|
+
module TestTooling
|
9
|
+
module Report
|
10
|
+
class UpdateScreenshotPath
|
11
|
+
def initialize(junit_files:)
|
12
|
+
@junit_files = junit_files
|
13
|
+
end
|
14
|
+
|
15
|
+
REGEX = %r{(?<gitlab_qa_run>gitlab-qa-run-.*?(?=/))/(?<gitlab_ce_ee_qa>gitlab-(?:ee|ce)-qa-.*?(?=/))}
|
16
|
+
CONTAINER_PATH = File.join('/home', 'gitlab', 'qa', 'tmp').freeze
|
17
|
+
|
18
|
+
def invoke!
|
19
|
+
Dir.glob(junit_files).each do |junit_file|
|
20
|
+
match_data = junit_file.match(REGEX)
|
21
|
+
next unless match_data
|
22
|
+
|
23
|
+
host_relative_path = "#{match_data[:gitlab_qa_run]}/#{match_data[:gitlab_ce_ee_qa]}"
|
24
|
+
|
25
|
+
rewrite_schreenshot_paths_in_junit_file(junit_file, host_relative_path)
|
26
|
+
rewrite_schreenshot_paths_in_json_file(junit_file.gsub('.xml', '.json'), host_relative_path)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :junit_files
|
33
|
+
|
34
|
+
def rewrite_schreenshot_paths_in_junit_file(junit_file, host_relative_path)
|
35
|
+
File.write(
|
36
|
+
junit_file,
|
37
|
+
rewrite_each_junit_screenshot_path(junit_file, host_relative_path).to_s
|
38
|
+
)
|
39
|
+
|
40
|
+
puts "Saved #{junit_file}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def rewrite_schreenshot_paths_in_json_file(json_file, host_relative_path)
|
44
|
+
File.write(
|
45
|
+
json_file,
|
46
|
+
JSON.pretty_generate(
|
47
|
+
rewrite_each_json_screenshot_path(json_file, host_relative_path)
|
48
|
+
)
|
49
|
+
)
|
50
|
+
|
51
|
+
puts "Saved #{json_file}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def rewrite_each_junit_screenshot_path(junit_file, host_relative_path)
|
55
|
+
Nokogiri::XML(File.open(junit_file)).tap do |report|
|
56
|
+
report.xpath('//system-out').each do |system_out|
|
57
|
+
system_out.content = remove_container_absolute_path_prefix(system_out.content, host_relative_path)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def rewrite_each_json_screenshot_path(json_file, host_relative_path)
|
63
|
+
JSON.parse(File.read(json_file)).tap do |report|
|
64
|
+
examples = report['examples']
|
65
|
+
|
66
|
+
examples.each do |example|
|
67
|
+
next unless example['screenshot'].present?
|
68
|
+
|
69
|
+
example['screenshot']['image'] =
|
70
|
+
remove_container_absolute_path_prefix(example.dig('screenshot', 'image'), host_relative_path)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def remove_container_absolute_path_prefix(image_container_absolute_path, host_relative_path)
|
76
|
+
image_container_absolute_path.gsub(CONTAINER_PATH, host_relative_path)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module GitlabQuality
|
7
|
+
module TestTooling
|
8
|
+
module Runtime
|
9
|
+
module Env
|
10
|
+
extend self
|
11
|
+
using Rainbow
|
12
|
+
|
13
|
+
ENV_VARIABLES = {
|
14
|
+
'GITLAB_API_BASE' => :api_base,
|
15
|
+
'GITLAB_QA_ISSUE_URL' => :qa_issue_url,
|
16
|
+
'GITLAB_CI_API_TOKEN' => :gitlab_ci_api_token,
|
17
|
+
'CI_COMMIT_REF_NAME' => :ci_commit_ref_name,
|
18
|
+
'CI_JOB_NAME' => :ci_job_name,
|
19
|
+
'CI_JOB_URL' => :ci_job_url,
|
20
|
+
'CI_PROJECT_ID' => :ci_project_id,
|
21
|
+
'CI_PROJECT_NAME' => :ci_project_name,
|
22
|
+
'CI_PIPELINE_ID' => :ci_pipeline_id,
|
23
|
+
'CI_PIPELINE_URL' => :ci_pipeline_url,
|
24
|
+
'CI_API_V4_URL' => :ci_api_v4_url,
|
25
|
+
'SLACK_QA_CHANNEL' => :slack_qa_channel,
|
26
|
+
'DEPLOY_VERSION' => :deploy_version
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
ENV_VARIABLES.each do |env_name, method_name|
|
30
|
+
define_method(method_name) do
|
31
|
+
env_var_value_if_defined(env_name) || (instance_variable_get("@#{method_name}") if instance_variable_defined?("@#{method_name}"))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_level
|
36
|
+
env_var_value_if_defined('QA_LOG_LEVEL')&.upcase || 'INFO'
|
37
|
+
end
|
38
|
+
|
39
|
+
def log_path
|
40
|
+
env_var_value_if_defined('QA_LOG_PATH') || host_artifacts_dir
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_branch
|
44
|
+
env_var_value_if_defined('QA_DEFAULT_BRANCH') || 'main'
|
45
|
+
end
|
46
|
+
|
47
|
+
def gitlab_api_base
|
48
|
+
env_var_value_if_defined('GITLAB_API_BASE') || 'https://gitlab.com/api/v4'
|
49
|
+
end
|
50
|
+
|
51
|
+
def pipeline_from_project_name
|
52
|
+
ci_project_name.to_s.start_with?('gitlab') ? default_branch : ci_project_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def run_id
|
56
|
+
@run_id ||= "gitlab-qa-run-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}-#{SecureRandom.hex(4)}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def colorized_logs?
|
60
|
+
enabled?(ENV.fetch('COLORIZED_LOGS', nil), default: false)
|
61
|
+
end
|
62
|
+
|
63
|
+
def deploy_environment
|
64
|
+
env_var_value_if_defined('DEPLOY_ENVIRONMENT') || pipeline_from_project_name
|
65
|
+
end
|
66
|
+
|
67
|
+
def host_artifacts_dir
|
68
|
+
@host_artifacts_dir ||= File.join(
|
69
|
+
env_var_value_if_defined('QA_ARTIFACTS_DIR') || '/tmp/gitlab-qa', Runtime::Env.run_id
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def qa_run_type
|
74
|
+
return env_var_value_if_defined('QA_RUN_TYPE') if env_var_value_valid?('QA_RUN_TYPE')
|
75
|
+
|
76
|
+
live_envs = %w[staging staging-canary staging-ref canary preprod production]
|
77
|
+
return unless live_envs.include?(ci_project_name)
|
78
|
+
|
79
|
+
test_subset = if env_var_value_if_defined('NO_ADMIN') == 'true'
|
80
|
+
'sanity-no-admin'
|
81
|
+
elsif env_var_value_if_defined('SMOKE_ONLY') == 'true'
|
82
|
+
'sanity'
|
83
|
+
else
|
84
|
+
'full'
|
85
|
+
end
|
86
|
+
|
87
|
+
"#{ci_project_name}-#{test_subset}"
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def enabled?(value, default: true)
|
93
|
+
return default if value.nil?
|
94
|
+
|
95
|
+
(value =~ /^(false|no|0)$/i) != 0
|
96
|
+
end
|
97
|
+
|
98
|
+
def env_var_value_valid?(variable)
|
99
|
+
!ENV[variable].blank?
|
100
|
+
end
|
101
|
+
|
102
|
+
def env_var_value_if_defined(variable)
|
103
|
+
return ENV.fetch(variable) if env_var_value_valid?(variable)
|
104
|
+
end
|
105
|
+
|
106
|
+
def env_var_name_if_defined(variable)
|
107
|
+
# Pass through the variables if they are defined and not empty in the environment
|
108
|
+
return "$#{variable}" if env_var_value_valid?(variable)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rainbow'
|
6
|
+
require 'active_support/logger'
|
7
|
+
|
8
|
+
module GitlabQuality
|
9
|
+
module TestTooling
|
10
|
+
module Runtime
|
11
|
+
class Logger
|
12
|
+
extend SingleForwardable
|
13
|
+
|
14
|
+
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
|
15
|
+
|
16
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
17
|
+
LEVEL_COLORS = {
|
18
|
+
"DEBUG" => :magenta,
|
19
|
+
"INFO" => :green,
|
20
|
+
"WARN" => :yellow,
|
21
|
+
"ERROR" => :red,
|
22
|
+
"FATAL" => :indianred
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
Rainbow.enabled = Runtime::Env.colorized_logs?
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# Combined logger instance
|
29
|
+
#
|
30
|
+
# @param [String] source
|
31
|
+
# @return [ActiveSupport::Logger]
|
32
|
+
def logger(source: 'Gitlab QA')
|
33
|
+
@logger ||= begin
|
34
|
+
log_path = Env.log_path
|
35
|
+
::FileUtils.mkdir_p(log_path)
|
36
|
+
|
37
|
+
console_log = console_logger(source: source, level: Env.log_level)
|
38
|
+
file_log = file_logger(source: source, path: log_path)
|
39
|
+
|
40
|
+
console_log.extend(ActiveSupport::Logger.broadcast(file_log))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Console logger instance
|
47
|
+
#
|
48
|
+
# @param [String] source
|
49
|
+
# @param [<Symbol, String>] level
|
50
|
+
# @return [ActiveSupport::Logger]
|
51
|
+
def console_logger(source:, level:)
|
52
|
+
ActiveSupport::Logger.new($stdout, level: level, datetime_format: TIME_FORMAT).tap do |logger|
|
53
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
54
|
+
msg_prefix = message_prefix(datetime, source, severity)
|
55
|
+
|
56
|
+
Rainbow(msg_prefix).public_send(LEVEL_COLORS.fetch(severity, :silver)) + "#{msg}\n" # rubocop:disable GitlabSecurity/PublicSend
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# File logger
|
62
|
+
#
|
63
|
+
# @param [String] source
|
64
|
+
# @param [String] path
|
65
|
+
# @return [ActiveSupport::Logger]
|
66
|
+
def file_logger(source:, path:)
|
67
|
+
log_file = "#{path}/#{source.downcase.tr(' ', '-')}.log"
|
68
|
+
|
69
|
+
ActiveSupport::Logger.new(log_file, level: :debug, datetime_format: TIME_FORMAT).tap do |logger|
|
70
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
71
|
+
msg_prefix = message_prefix(datetime, source, severity)
|
72
|
+
|
73
|
+
"#{msg_prefix}#{msg}\n".gsub(/\e\[(\d+)(?:;\d+)*m/, "")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Log message prefix
|
79
|
+
#
|
80
|
+
# @note when outputted, the date will be formatted as "Jun 07 2022 11:30:00 UTC"
|
81
|
+
# @param [DateTime] date
|
82
|
+
# @param [String] source
|
83
|
+
# @param [String] severity
|
84
|
+
# @return [String]
|
85
|
+
def message_prefix(date, source, severity)
|
86
|
+
"[#{date.strftime('%h %d %Y %H:%M:%S %Z')} (#{source})] #{severity.ljust(5)} -- "
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Runtime
|
6
|
+
class TokenFinder
|
7
|
+
def self.find_token!(token, suffix: nil)
|
8
|
+
new(token, suffix).find_token!
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :token, :suffix
|
12
|
+
|
13
|
+
def initialize(token, suffix)
|
14
|
+
@token = token
|
15
|
+
@suffix = suffix
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_token!
|
19
|
+
find_token_from_attrs || find_token_from_env || find_token_from_file
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_token_from_attrs
|
23
|
+
token
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_token_from_env
|
27
|
+
Env.gitlab_ci_api_token
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_token_from_file
|
31
|
+
@token_from_file ||= File.read(token_file_path).strip
|
32
|
+
rescue Errno::ENOENT
|
33
|
+
fail "Please provide a valid access token with the `-t/--token` option, the `GITLAB_CI_API_TOKEN` environment variable, or in the `#{token_file_path}` file!"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def token_file_path
|
39
|
+
@token_file_path ||= File.expand_path("../api_token#{"_#{suffix}" if suffix}", __dir__)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Slack
|
6
|
+
class PostToSlack
|
7
|
+
def initialize(slack_webhook_url:, channel:, message:, username:, icon_emoji:)
|
8
|
+
@slack_webhook_url = slack_webhook_url
|
9
|
+
@channel = channel
|
10
|
+
@message = message
|
11
|
+
@username = username
|
12
|
+
@icon_emoji = icon_emoji
|
13
|
+
end
|
14
|
+
|
15
|
+
def invoke!
|
16
|
+
params = {}
|
17
|
+
params['channel'] = channel
|
18
|
+
params['username'] = username
|
19
|
+
params['icon_emoji'] = icon_emoji
|
20
|
+
params['text'] = message
|
21
|
+
|
22
|
+
Support::HttpRequest.make_http_request(
|
23
|
+
method: 'post',
|
24
|
+
url: slack_webhook_url,
|
25
|
+
params: params,
|
26
|
+
show_response: true
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :slack_webhook_url, :channel, :message, :username, :icon_emoji
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'table_print'
|
5
|
+
|
6
|
+
module GitlabQuality
|
7
|
+
module TestTooling
|
8
|
+
class SummaryTable
|
9
|
+
def self.create(input_files:)
|
10
|
+
"```\n#{TablePrint::Printer.table_print(collect_results(input_files))}```\n"
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/AbcSize
|
14
|
+
def self.collect_results(input_files)
|
15
|
+
stage_wise_results = []
|
16
|
+
|
17
|
+
Dir.glob(input_files).each do |report_file|
|
18
|
+
stage_hash = {}
|
19
|
+
stage_hash["Dev Stage"] = File.basename(report_file, ".*").capitalize
|
20
|
+
|
21
|
+
report_stats = Nokogiri::XML(File.open(report_file)).children[0].attributes
|
22
|
+
|
23
|
+
stage_hash["Total"] = report_stats["tests"].value
|
24
|
+
stage_hash["Failures"] = report_stats["failures"].value
|
25
|
+
stage_hash["Errors"] = report_stats["errors"].value
|
26
|
+
stage_hash["Skipped"] = report_stats["skipped"].value
|
27
|
+
stage_hash["Result"] = result_emoji(report_stats)
|
28
|
+
|
29
|
+
stage_wise_results << stage_hash
|
30
|
+
end
|
31
|
+
|
32
|
+
stage_wise_results
|
33
|
+
end
|
34
|
+
# rubocop:enable Metrics/AbcSize
|
35
|
+
|
36
|
+
def self.result_emoji(report_stats)
|
37
|
+
report_stats["failures"].value.to_i.positive? || report_stats["errors"].value.to_i.positive? ? "❌" : "✅"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module GitlabQuality
|
7
|
+
module TestTooling
|
8
|
+
module Support
|
9
|
+
class HttpRequest
|
10
|
+
# rubocop:disable Metrics/AbcSize
|
11
|
+
def self.make_http_request(method: 'get', url: nil, params: {}, headers: {}, show_response: false, fail_on_error: true)
|
12
|
+
raise "URL not defined for making request. Exiting..." unless url
|
13
|
+
|
14
|
+
res = HTTP.follow.method(method).call(url, json: params, headers: headers)
|
15
|
+
|
16
|
+
if show_response
|
17
|
+
if res.content_type.mime_type == "application/json"
|
18
|
+
res_body = JSON.parse(res.body.to_s)
|
19
|
+
pp res_body
|
20
|
+
else
|
21
|
+
res_body = res.body.to_s
|
22
|
+
puts res_body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
raise "#{method.upcase} request failed!\nCode: #{res.code}\nResponse: #{res.body}\n" if fail_on_error && !res.status.success?
|
27
|
+
|
28
|
+
res
|
29
|
+
end
|
30
|
+
# rubocop:enable Metrics/AbcSize
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|