From 01ed92c377ef8b2b117776decc97f307c9c3a160 Mon Sep 17 00:00:00 2001 From: Jay McCure <jmccure@gitlab.com> Date: Tue, 14 Nov 2023 18:55:55 +0000 Subject: [PATCH] E2E test: group reliable report by product group --- qa/qa/tools/reliable_report.rb | 160 +++++++++++++++----------- qa/spec/tools/reliable_report_spec.rb | 94 +++++++++++---- 2 files changed, 161 insertions(+), 93 deletions(-) diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb index a0933d07166d..de8fbf7f044e 100644 --- a/qa/qa/tools/reliable_report.rb +++ b/qa/qa/tools/reliable_report.rb @@ -59,14 +59,14 @@ def self.run(range: 14, report_in_issue_and_slack: "false") # # @return [void] def print_report - puts "#{stable_summary_table}\n\n" + puts "#{summary_table(stable: true)}\n\n" puts "Total amount: #{stable_test_runs.sum { |_k, v| v.count }}\n\n" - stable_results_tables.each { |stage, table| puts "#{table}\n\n" } + print_results(stable_results_tables) return puts("No unstable reliable tests present!".colorize(:yellow)) if unstable_reliable_test_runs.empty? - puts "#{unstable_summary_table}\n\n" + puts "#{summary_table(stable: false)}\n\n" puts "Total amount: #{unstable_reliable_test_runs.sum { |_k, v| v.count }}\n\n" - unstable_reliable_results_tables.each { |stage, table| puts "#{table}\n\n" } + print_results(unstable_reliable_results_tables) end # Create report issue @@ -89,8 +89,8 @@ def report_in_issue_and_slack notifier.post( icon_emoji: ":tanuki-protect:", text: <<~TEXT - ```#{stable_summary_table}``` - ```#{unstable_summary_table}``` + ```#{summary_table(stable: true)}``` + ```#{summary_table(stable: false)}``` #{web_url} TEXT @@ -173,39 +173,30 @@ def report_issue_body issue = [] issue << "[[_TOC_]]" issue << "# Candidates for promotion to reliable #{execution_interval}" - issue << "Total amount: **#{stable_test_runs.sum { |_k, v| v.count }}**" - issue << stable_summary_table(markdown: true).to_s + issue << "Total amount: **#{test_count(stable_test_runs)}**" + issue << summary_table(markdown: true, stable: true).to_s issue << results_markdown(:stable) return issue.join("\n\n") if unstable_reliable_test_runs.empty? issue << "# Reliable specs with failures #{execution_interval}" - issue << "Total amount: **#{unstable_reliable_test_runs.sum { |_k, v| v.count }}**" - issue << unstable_summary_table(markdown: true).to_s + issue << "Total amount: **#{test_count(unstable_reliable_test_runs)}**" + issue << summary_table(markdown: true, stable: false).to_s issue << results_markdown(:unstable) issue.join("\n\n") end - # Stable spec summary table + # Spec summary table # # @param [Boolean] markdown + # @param [Boolean] stable # @return [Terminal::Table] - def stable_summary_table(markdown: false) + def summary_table(markdown: false, stable: true) + test_runs = stable ? stable_test_runs : unstable_reliable_test_runs terminal_table( - rows: stable_test_runs.map { |stage, specs| [stage, specs.length] }, - title: "Stable spec summary for past #{range} days".ljust(50), - headings: %w[STAGE COUNT], - markdown: markdown - ) - end - - # Unstable reliable summary table - # - # @param [Boolean] markdown - # @return [Terminal::Table] - def unstable_summary_table(markdown: false) - terminal_table( - rows: unstable_reliable_test_runs.map { |stage, specs| [stage, specs.length] }, - title: "Unstable spec summary for past #{range} days".ljust(50), + rows: test_runs.map do |stage, stage_specs| + [stage, stage_specs.sum { |_k, group_specs| group_specs.length }] + end, + title: "#{stable ? 'Stable' : 'Unstable'} spec summary for past #{range} days".ljust(50), headings: %w[STAGE COUNT], markdown: markdown ) @@ -233,41 +224,51 @@ def unstable_reliable_results_tables(markdown: false) # @return [String] def results_markdown(type) runs = type == :stable ? stable_test_runs : unstable_reliable_test_runs - results_tables(type, markdown: true).map do |stage, table| - <<~STAGE.strip - ## #{stage} (#{runs[stage].count}) + results_tables(type, markdown: true).map do |stage, group_tables| + markdown = "## #{stage.capitalize} (#{runs[stage].sum { |_k, group_runs| group_runs.count }})\n\n" - <details> - <summary>Executions table</summary> + markdown << group_tables.map { |product_group, table| group_results_markdown(product_group, table) }.join + end.join("\n\n") + end - #{table} + # Markdown formatted group results table + # + # @param [String] product_group + # @param [Terminal::Table] table + # @return [String] + def group_results_markdown(product_group, table) + <<~MARKDOWN.chomp + <details> + <summary>Executions table ~"group::#{product_group.tr('_', ' ')}" (#{table.rows.size})</summary> - </details> - STAGE - end.join("\n\n") + #{table} + + </details> + MARKDOWN end # Results table # # @param [Symbol] type result type - :stable, :unstable # @param [Boolean] markdown - # @return [Hash<Symbol, Terminal::Table>] + # @return [Hash<String, Hash<String, Terminal::Table>>] grouped by stage and product_group def results_tables(type, markdown: false) (type == :stable ? stable_test_runs : unstable_reliable_test_runs).to_h do |stage, specs| - headings = ["name", "runs", "failures", "failure rate"] - - [stage, terminal_table( - title: "Top #{type} specs in '#{stage}' stage for past #{range} days", - headings: headings.map(&:upcase), - markdown: markdown, - rows: specs.map do |k, v| - [ - name_column(name: k, file: v[:file], link: v[:link], - exceptions_and_job_urls: v[:exceptions_and_job_urls], markdown: markdown), - *table_params(v.values) - ] - end - )] + headings = ['NAME', 'RUNS', 'FAILURES', 'FAILURE RATE'].freeze + [stage, specs.transform_values do |group_specs| + terminal_table( + title: "Top #{type} specs in '#{stage}::#{specs.key(group_specs)}' group for past #{range} days", + headings: headings, + markdown: markdown, + rows: group_specs.map do |name, result| + [ + name_column(name: name, file: result[:file], link: result[:link], + exceptions_and_job_urls: result[:exceptions_and_job_urls], markdown: markdown), + *table_params(result.values) + ] + end + ) + end] end end @@ -276,14 +277,14 @@ def results_tables(type, markdown: false) # @return [Hash] def stable_test_runs @top_stable ||= begin - stable_specs = test_runs(reliable: false).transform_values do |specs| - specs - .reject { |k, v| v[:failure_rate] != 0 } - .sort_by { |k, v| -v[:runs] } - .to_h + stable_specs = test_runs(reliable: false).each do |stage, stage_specs| + stage_specs.transform_values! do |group_specs| + group_specs.reject { |k, v| v[:failure_rate] != 0 } + .sort_by { |k, v| -v[:runs] } + .to_h + end end - - stable_specs.reject { |k, v| v.empty? } + stable_specs.transform_values { |v| v.reject { |_, v| v.empty? } }.reject { |_, v| v.empty? } end end @@ -292,14 +293,26 @@ def stable_test_runs # @return [Hash] def unstable_reliable_test_runs @top_unstable_reliable ||= begin - unstable = test_runs(reliable: true).transform_values do |specs| - specs - .reject { |k, v| v[:failure_rate] == 0 } - .sort_by { |k, v| -v[:failure_rate] } - .to_h + unstable = test_runs(reliable: true).each do |_stage, stage_specs| + stage_specs.transform_values! do |group_specs| + group_specs.reject { |_, v| v[:failure_rate] == 0 } + .sort_by { |_, v| -v[:failure_rate] } + .to_h + end end + unstable.transform_values { |v| v.reject { |_, v| v.empty? } }.reject { |_, v| v.empty? } + end + end - unstable.reject { |k, v| v.empty? } + def print_results(results) + results.each do |_stage, stage_results| + stage_results.each_value { |group_results_table| puts "#{group_results_table}\n\n" } + end + end + + def test_count(test_runs) + test_runs.sum do |_stage, stage_results| + stage_results.sum { |_product_group, group_results| group_results.count } end end @@ -368,16 +381,14 @@ def test_runs(reliable:) all_runs.each_with_object(Hash.new { |hsh, key| hsh[key] = {} }) do |table, result| records = table.records.sort_by { |record| record.values["_time"] } - # skip specs that executed less time than defined by range or stopped executing before report date - # offset 1 day due to how schedulers are configured and first run can be 1 day later - next if (Date.today - Date.parse(records.first.values["_time"])).to_i < (range - 1) - next if (Date.today - Date.parse(records.last.values["_time"])).to_i > 1 + next if within_execution_range(records.first.values["_time"], records.last.values["_time"]) last_record = records.last.values name = last_record["name"] file = last_record["file_path"].split("/").last link = FEATURES_DIR + last_record["file_path"] stage = last_record["stage"] || "unknown" + product_group = last_record["product_group"] || "unknown" runs = records.count @@ -394,7 +405,8 @@ def test_runs(reliable:) [r.values["failure_exception"], r.values["job_url"]] end - result[stage][name] = { + result[stage][product_group] ||= {} + result[stage][product_group][name] = { file: file, link: link, runs: runs, @@ -415,6 +427,16 @@ def allowed_failure?(failure_exception) ALLOWED_EXCEPTION_PATTERNS.any? { |pattern| pattern.match?(failure_exception) } end + # Returns true if first_time is before our range, or if last_time is before report date + # offset 1 day due to how schedulers are configured and first run can be 1 day later + # + # @param [String] first_time + # @param [String] last_time + # @return [Boolean] + def within_execution_range(first_time, last_time) + (Date.today - Date.parse(first_time)).to_i < (range - 1) || (Date.today - Date.parse(last_time)).to_i > 1 + end + # Flux query # # @param [Boolean] reliable diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb index cf5c9dea7943..eca3baffdf00 100644 --- a/qa/spec/tools/reliable_report_spec.rb +++ b/qa/spec/tools/reliable_report_spec.rb @@ -18,10 +18,19 @@ let(:runs) do values = { - "name" => "stable spec", + "name" => "stable spec1", + "status" => "passed", + "file_path" => "some/spec.rb", + "stage" => "create", + "product_group" => "code_review", + "_time" => time + } + more_values = { + "name" => "stable spec2", "status" => "passed", "file_path" => "some/spec.rb", "stage" => "manage", + "product_group" => "import_and_integrate", "_time" => time } [ @@ -32,6 +41,14 @@ instance_double("InfluxDB2::FluxRecord", values: values), instance_double("InfluxDB2::FluxRecord", values: values.merge({ "_time" => Time.now.to_s })) ] + ), + instance_double( + "InfluxDB2::FluxTable", + records: [ + instance_double("InfluxDB2::FluxRecord", values: more_values), + instance_double("InfluxDB2::FluxRecord", values: more_values), + instance_double("InfluxDB2::FluxRecord", values: more_values.merge({ "_time" => Time.now.to_s })) + ] ) ] end @@ -42,6 +59,17 @@ "status" => "failed", "file_path" => "some/spec.rb", "stage" => "create", + "product_group" => "code_review", + "failure_exception" => failure_message, + "job_url" => "https://job/url", + "_time" => time + } + more_values = { + "name" => "unstable spec", + "status" => "failed", + "file_path" => "some/spec.rb", + "stage" => "manage", + "product_group" => "import_and_integrate", "failure_exception" => failure_message, "job_url" => "https://job/url", "_time" => time @@ -54,6 +82,14 @@ instance_double("InfluxDB2::FluxRecord", values: values), instance_double("InfluxDB2::FluxRecord", values: values.merge({ "_time" => Time.now.to_s })) ] + ), + instance_double( + "InfluxDB2::FluxTable", + records: [ + instance_double("InfluxDB2::FluxRecord", values: { **more_values, "status" => "passed" }), + instance_double("InfluxDB2::FluxRecord", values: more_values), + instance_double("InfluxDB2::FluxRecord", values: more_values.merge({ "_time" => Time.now.to_s })) + ] ) ] end @@ -89,14 +125,12 @@ def flux_query(reliable:) QUERY end - def markdown_section(summary, result, stage, type) + def expected_stage_markdown(result, stage, product_group, type) <<~SECTION.strip - #{summary_table(summary, type, true)} - - ## #{stage} (1) + ## #{stage.capitalize} (1) <details> - <summary>Executions table</summary> + <summary>Executions table ~\"group::#{product_group}\" (1)</summary> #{table(result, ['NAME', 'RUNS', 'FAILURES', 'FAILURE RATE'], "Top #{type} specs in '#{stage}' stage for past #{range} days", true)} @@ -104,7 +138,7 @@ def markdown_section(summary, result, stage, type) SECTION end - def summary_table(summary, type, markdown = false) + def expected_summary_table(summary, type, markdown = false) table(summary, %w[STAGE COUNT], "#{type.capitalize} spec summary for past #{range} days".ljust(50), markdown) end @@ -241,28 +275,36 @@ def exceptions_markdown(exceptions_and_job_urls) let(:expected_issue_body) do <<~TXT.strip - [[_TOC_]] + [[_TOC_]] + + # Candidates for promotion to reliable (#{Date.today - range} - #{Date.today}) + + Total amount: **2** + + #{expected_summary_table([['create', 1], ['manage', 1]], :stable, true)} - # Candidates for promotion to reliable (#{Date.today - range} - #{Date.today}) + #{expected_stage_markdown([[name_column('stable spec1'), 3, 0, '0%']], 'create', 'code review', :stable)} - Total amount: **1** + #{expected_stage_markdown([[name_column('stable spec2'), 3, 0, '0%']], 'manage', 'import and integrate', :stable)} - #{markdown_section([['manage', 1]], [[name_column('stable spec'), 3, 0, '0%']], 'manage', 'stable')} + # Reliable specs with failures (#{Date.today - range} - #{Date.today}) - # Reliable specs with failures (#{Date.today - range} - #{Date.today}) + Total amount: **2** - Total amount: **1** + #{expected_summary_table([['create', 1], ['manage', 1]], :unstable, true)} - #{markdown_section([['create', 1]], [[name_column('unstable spec', { failure_message => 'https://job/url' }), 3, 2, '66.67%']], 'create', 'unstable')} + #{expected_stage_markdown([[name_column('unstable spec', { failure_message => 'https://job/url' }), 3, 2, '66.67%']], 'create', 'code review', :unstable)} + + #{expected_stage_markdown([[name_column('unstable spec', { failure_message => 'https://job/url' }), 3, 2, '66.67%']], 'manage', 'import and integrate', :unstable)} TXT end let(:expected_slack_text) do <<~TEXT - ```#{summary_table([['manage', 1]], 'stable')}``` - ```#{summary_table([['create', 1]], 'unstable')}``` + ```#{expected_summary_table([['create', 1], ['manage', 1]], :stable)}``` + ```#{expected_summary_table([['create', 1], ['manage', 1]], :unstable)}``` - #{issue_url} + #{issue_url} TEXT end @@ -274,22 +316,26 @@ def exceptions_markdown(exceptions_and_job_urls) let(:expected_issue_body) do <<~TXT.strip - [[_TOC_]] + [[_TOC_]] + + # Candidates for promotion to reliable (#{Date.today - range} - #{Date.today}) + + Total amount: **2** - # Candidates for promotion to reliable (#{Date.today - range} - #{Date.today}) + #{expected_summary_table([['create', 1], ['manage', 1]], :stable, true)} - Total amount: **1** + #{expected_stage_markdown([[name_column('stable spec1'), 3, 0, '0%']], 'create', 'code review', :stable)} - #{markdown_section([['manage', 1]], [[name_column('stable spec'), 3, 0, '0%']], 'manage', 'stable')} + #{expected_stage_markdown([[name_column('stable spec2'), 3, 0, '0%']], 'manage', 'import and integrate', :stable)} TXT end let(:expected_slack_text) do <<~TEXT - ```#{summary_table([['manage', 1]], 'stable')}``` - ```#{summary_table([], 'unstable')}``` + ```#{expected_summary_table([['create', 1], ['manage', 1]], :stable)}``` + ```#{expected_summary_table([], :unstable)}``` - #{issue_url} + #{issue_url} TEXT end -- GitLab